Search code examples
c#imageuploadlinkedin-apirestsharp

LinkedIn API image upload reliably PROCESSING_FAILED


I'm having some difficulty with the image upload feature, and I'm hoping somebody already knows the answer... I'm following these steps:

  1. POST to initialise an upload (and get the upload URL)
  2. PUT the image to the upload URL
  3. GET the status of the image (polling until either AVAILABLE or PROCESSING_FAILED)
  4. Include the images in either a content.media or content.multiImage in a post.

I reliably get PROCESSING_FAILED for the image status, so I'm halted at step 3. I'll share some more detail below, and I'd be glad of anyone's insights... Thanks!

Notes

I'm working in C# with the RestSharp library, but it should be pretty clear what I'm doing, and so probably doesn't matter which language you're familiar with if you can see the error in my method...

My app has been granted access to the Community Management API (development tier), and I'm using a token with w_member_social and w_organization_social scopes.

I own the page that I'm trying to post to, and the organisation urn is correct for that page.

I'm working with these documentation pages:

Detail

Where possible, I've simplified the code below by inlining strings, etc. so it's a little easier to see what I'm doing...

0 - Initialise

First, I initialise the client with the most commonly used base url:

var client = new RestClient("https://api.linkedin.com/rest");

1 - Initialise the upload

var imgRequest = new RestRequest($"images?action=initializeUpload", Method.Post);
imgRequest.AddHeader("Authorization", $"Bearer {token}");
imgRequest.AddHeader("Content-Type", "application/json");
imgRequest.AddHeader("X-Restli-Protocol-Version", "2.0.0");
imgRequest.AddHeader("LinkedIn-Version","202309");
var imgRequestBody = new { initializeUploadRequest = new { owner = author }};
imgRequest.AddJsonBody(imgRequestBody);
var imgResponse = await client!.ExecuteAsync(imgRequest);

This returns a success response and I can extract the URL from the JSON:

var imgResponseData = JsonConvert.DeserializeObject<LinkedInImageResponse>(imgResponse.Content!);
var uploadUrl = imgResponseData!.value.uploadUrl;

2 - Upload the image

using (var uploadClient = new RestClient())
{
  var uploadRequest = new RestRequest(uploadUrl, Method.Put); // PUT is correct
  uploadRequest.AddHeader("Authorization", $"Bearer {token}");
  uploadRequest.AddHeader("Content-Type", "image/png"); // TODO: get this from the image
  // uploadRequest.AddHeader("X-Restli-Protocol-Version", "2.0.0");
  // uploadRequest.AddHeader("LinkedIn-Version","202309");
  var imgUploadStream = await image.GetStreamAsync();
  uploadRequest.AddFile($"image_{++iCounter}", () => imgUploadStream, image.Filename);
  var uploadResponse = await uploadClient.ExecuteAsync(uploadRequest);
  if (!uploadResponse.IsSuccessful) throw new Exception(); // simplified
}

This returns a success response, and so I believe the image has uploaded successfully.

3 - Poll for the image status

This is where things start to go a bit awry...

var imageOk = false;
do
{
  Thread.Sleep(5000); // 5s poll... (ugh!)
  var pollRequest = new RestRequest($"images/{imgResponseData!.value.image}", Method.Get);
  pollRequest.AddHeader("Authorization", $"Bearer {token}");
  pollRequest.AddHeader("LinkedIn-Version","202309");
  var pollResponse = await client!.ExecuteAsync(pollRequest);
  var pollData = JsonConvert.DeserializeObject<LinkedInPollResponse>(pollResponse.Content!);
  imageOk = pollData?.status == "AVAILABLE";
  var imageFailed = pollData?.status == "PROCESSING_FAILED";
  if (imageFailed) { throw new Exception($"Image status for post {t}: {pollData?.status}", new Exception(Summarise(pollRequest, null) + Summarise(pollResponse))); }
  if (++attempts > 5) throw new Exception($"Cannot poll image status for post {t} - too many attempts");
} while (!imageOk);

This uses the common client again, with the base url initialised in step 0.

I get a 200 (success) response for the request, and the content contains status PROCESSING_FAILED:

{
  "owner":"urn:li:organization:19321500",
  "status":"PROCESSING_FAILED",
  "id":"urn:li:image:D4E10AQHZqWNBn_7gOQ"
}

The documentation says:

PROCESSING_FAILED - Processing failed due to client error such as file size too large, unsupported file format, internal error (e.g., performance issue, database error, network failure), or other issue.

The image is a small (291kb) PNG dimensions: 432x430. I'd be surprised if there was anything wrong with it - it renders fine under ordinary circumstances (including when manually creating a post).

I'd be grateful for any insights...


Solution

  • Having rubber ducked the whole thing rather intensely, I tried a few more keywords in my search and found this solution from j4rey, which answers the problem.

    TLDR: The issue is that I had included the file using uploadRequest.AddFile - which includes it as a part of a form post. LinkedIn wants the file as binary content.

    Here's my adaptation of their code - which works perfectly:

    using (var uploadClient = new HttpClient())
    {
      uploadClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
      var imageDataStream = await image.GetStreamAsync();
      var uploadContent = new StreamContent(imageDataStream);
      var uploadResponse = await uploadClient.PutAsync(uploadUrl, uploadContent);
      if (!uploadResponse.IsSuccessStatusCode) throw new Exception($"Cannot upload image for post {t}", new Exception(Summarise(uploadResponse)));
    }
    

    (NB. I switched away from RestSharp for the upload - it's not really a restful action anyway, and HttpClient is a familiar friend.)