You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用最新LinkedIn API发布图片帖失败,无错误返回

问题

我可以正常发布纯文本帖,但图片帖无法成功发布,且未收到任何错误提示。已遵循LinkedIn官方API文档,但响应中未包含文档提及的x-linkedin-id。以下是我的Next.js后端代码:

import dbConnect from "../../../lib/dbConnect";
import { getSession } from "next-auth/react";
import formidable from "formidable";
const fs = require("fs").promises;

export const config = {
  api: { bodyParser: false },
};

export default async function handler(req, res) {
  const { method } = req;

  await dbConnect();

  switch (method) {
    case "POST":
      try {
        const session = await getSession({ req });

        if (!session || !session.accessToken) {
          console.error("No session or accessToken found.");
          return res.status(401).json({ error: "Not authorized" });
        }

        const accessToken = session.accessToken;
        console.log("accessToken>>>", accessToken);

        const linkedinId = "myLinkedInID";

        const data = await new Promise(function (resolve, reject) {
          const form = new formidable.IncomingForm();
          form.uploadDir = "/tmp";
          form.keepExtensions = true;
          form.multiples = true;

          form.parse(req, async function (err, fields, files) {
            if (err) return reject(err);
            resolve({ fields, files });
          });
        });

        req.body = data.fields;

        let body = undefined;
        const fileArray = Object.values(data.files);

        switch (req.body.type) {
          case "text":
            body = createTextPost(req.body.content, linkedinId);
            break;

          case "singleImage":
            const tempAsset = await uploadFileToLinkedIn(
              fileArray[0],
              accessToken,
              "image",
              linkedinId
            );

            body = createSingleImagePost(req.body.content, tempAsset, linkedinId);
            break;

          default:
            return res.status(400).json({ error: "Invalid content type" });
        }

        const url = "https://api.linkedin.com/rest/posts";

        const response = await fetch(url, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            "Content-Type": "application/json",
            "X-Restli-Protocol-Version": "2.0.0",
            "LinkedIn-Version": "202307",
          },
          body: JSON.stringify(body),
        });

        console.log("Status>>>>", response.ok);
        if (response.ok) {
          console.log("headers>>>>", response.headers);

          const linkedinId = response.headers.get("x-linkedin-id");
          console.log("x-linkedin-id>>>>", linkedinId);

          if (response.bodyUsed) {
            const data = await response.json();
            console.error("data>>>>", data);
          }
        } else {
          const text = await response.text();
        }

        res.status(200).json({ success: true });
      } catch (error) {
        console.error("LinkedIn Error", error);
        res.status(500).json({ error: "Internal Server Error" });
      }
      break;
  }

  function createTextPost(content, linkedinId) {
    return {
      author: `urn:li:person:${linkedinId}`,
      commentary: content,
      visibility: "PUBLIC",
      distribution: {
        feedDistribution: "MAIN_FEED",
        targetEntities: [],
        thirdPartyDistributionChannels: [],
      },
      lifecycleState: "PUBLISHED",
      isReshareDisabledByAuthor: false,
    };
  }

  function createSingleImagePost(content, media, linkedinId) {
    return {
      author: `urn:li:person:${linkedinId}`,
      commentary: content,
      visibility: "PUBLIC",
      distribution: {
        feedDistribution: "MAIN_FEED",
        targetEntities: [],
        thirdPartyDistributionChannels: [],
      },
      content: {
        media: {
          altText: "testing for alt tags",
          id: media,
        },
      },
      lifecycleState: "PUBLISHED",
      isReshareDisabledByAuthor: false,
    };
  }

  async function registerUpload(accessToken, owner, fileSize, mediaType) {
    const registerUrl = "https://api.linkedin.com/rest/images?action=initializeUpload";

    const body = {
      initializeUploadRequest: {
        owner: owner,
      },
    };

    const response = await fetch(registerUrl, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "Content-Type": "application/json",
        "X-Restli-Protocol-Version": "2.0.0",
        "LinkedIn-Version": "202307",
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      response
        .text()
        .then((data) => {
          console.error("Failed to register:", data);
        })
        .catch((error) => {
          console.error("Error parsing response:", error);
        });
      throw new Error("Failed to register upload.");
    }

    return await response.json();
  }

  async function uploadMedia(accessToken, uploadUrl, fileContent, mimetype) {
    var type = "jpg";
    if (mimetype == "image/jpeg") {
      type = "jpeg";
    } else if (mimetype == "image/png" || mimetype == "png") {
      type = "png";
    } else if (mimetype == "image/gif" || mimetype == "gif") {
      type = "gif";
    } else if (mimetype == "image/svg+xml" || mimetype == "svg") {
      type = "svg";
    } else if (mimetype == "application/pdf" || mimetype == "pdf") {
      type = "pdf";
    }

    const obj = {
      data: fileContent,
      mimeType: `application/${mimetype}`,
    };

    try {
      const response = await fetch(uploadUrl, {
        method: "PUT",
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "X-Restli-Protocol-Version": "2.0.0",
          "LinkedIn-Version": "202307",
          "Content-Type": `multipart/form-data`,
        },
        body: fileContent,
      });

      if (!response.ok) {
        const data = await response.text();
        console.error("Failed to upload media:", data);
        throw new Error("Failed to upload media.");
      }
    } catch (error) {
      console.log("Upload Media Error>>>>", error);
    }
  }

  async function uploadFileToLinkedIn(element, accessToken, mediaType, linkedinId) {
    const owner = `urn:li:person:${linkedinId}`;
    const fileSize = element.size;
    let fileContent = await fs.readFile(element.filepath);
    let type = element.mimetype.split("/")[1];

    const registerResponse = await registerUpload(
      accessToken,
      owner,
      fileSize,
      mediaType
    );
    const uploadUrl = registerResponse.value.uploadUrl;
    const asset = registerResponse.value.image;

    try {
      await uploadMedia(accessToken, uploadUrl, fileContent, type);
    } catch (error) {
      console.log("Upload Media FUnction>>>>", error);
    }

    return asset;
  }
}
问题排查与修复方案

1. 媒体上传的Content-Type完全错误

你的uploadMedia函数存在两个关键错误:

  • 将图片上传的Content-Type设为multipart/form-data,这是表单上传的类型,直接上传图片应使用对应图片的MIME类型(如image/jpeg、image/png)。
  • 错误构造了application/${mimetype}的MIME类型,完全不符合图片格式要求。

修复代码:
修改uploadMedia函数,直接使用文件原始MIME类型,去掉多余类型判断:

async function uploadMedia(accessToken, uploadUrl, fileContent, actualMimeType) {
  try {
    const response = await fetch(uploadUrl, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "X-Restli-Protocol-Version": "2.0.0",
        "LinkedIn-Version": "202307",
        "Content-Type": actualMimeType, // 使用文件原始MIME类型
      },
      body: fileContent,
    });

    if (!response.ok) {
      const data = await response.text();
      console.error("Failed to upload media:", data);
      throw new Error("Failed to upload media.");
    }
  } catch (error) {
    console.log("Upload Media Error>>>>", error);
    throw error; // 抛出错误让上层感知上传失败
  }
}

同时修改uploadFileToLinkedIn中的调用,传入正确的MIME类型:

// 替换原type变量定义
const actualMimeType = element.mimetype;
// ...
await uploadMedia(accessToken, uploadUrl, fileContent, actualMimeType);

2. 图片帖子的Content结构不符合API要求

LinkedIn API要求图片帖子的content.media是数组类型,而非单个对象。你当前直接传递单个media对象,导致API无法正确识别。

修复代码:
修改createSingleImagePost函数:

function createSingleImagePost(content, media, linkedinId) {
  return {
    author: `urn:li:person:${linkedinId}`,
    commentary: content,
    visibility: "PUBLIC",
    distribution: {
      feedDistribution: "MAIN_FEED",
      targetEntities: [],
      thirdPartyDistributionChannels: [],
    },
    content: {
      media: [ // 改为数组格式
        {
          altText: "testing for alt tags",
          id: media,
        }
      ],
    },
    lifecycleState: "PUBLISHED",
    isReshareDisabledByAuthor: false,
  };
}

3. 未等待图片处理完成就发帖

LinkedIn上传图片后需要一段时间处理文件,状态变为READY才能用于发帖。你当前上传后直接返回asset,可能媒体仍处于PROCESSING状态,导致发帖失败。

修复代码:
添加轮询函数等待媒体就绪:

async function waitForMediaReady(accessToken, assetUrn) {
  const checkUrl = `https://api.linkedin.com/rest/images/${assetUrn.split(":").pop()}`;
  let retries = 10;
  let interval = 2000; // 每2秒检查一次

  while (retries > 0) {
    const response = await fetch(checkUrl, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        "X-Restli-Protocol-Version": "2.0.0",
        "LinkedIn-Version": "202307",
      },
    });

    if (response.ok) {
      const data = await response.json();
      if (data.status === "READY") {
        return true;
      } else if (data.status === "FAILED") {
        throw new Error("Media processing failed");
      }
    }

    retries--;
    await new Promise(resolve => setTimeout(resolve, interval));
  }

  throw new Error("Media processing timed out");
}

在uploadFileToLinkedIn中调用该函数:

await uploadMedia(accessToken, uploadUrl, fileContent, actualMimeType);
await waitForMediaReady(accessToken, asset); // 等待媒体处理完成

4. 响应处理逻辑错误

你在处理posts接口响应时,if (response.bodyUsed)的判断逻辑完全反转——bodyUsed为true表示body已被读取,此时再读取会报错。此外,即使response.ok,也应读取响应内容获取post的ID或其他信息。

修复代码:

if (response.ok) {
  console.log("headers>>>>", response.headers);
  const linkedinId = response.headers.get("x-linkedin-id");
  console.log("x-linkedin-id>>>>", linkedinId);

  // 直接读取响应内容
  const data = await response.json();
  console.log("Post response data>>>>", data);
} else {
  const text = await response.text();
  console.error("Error response>>>>", text); // 开启错误日志便于排查
  throw new Error(`Post creation failed: ${text}`);
}

5. 错误处理不彻底

uploadMedia的catch块仅打印错误但未抛出,导致上层无法感知上传失败,仍会使用无效asset发帖。该问题已在第1点修复中通过throw error解决。


内容的提问来源于stack exchange,提问作者Anirudh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 03:25:55