yikart/AiToEarn · warning · ThreadsPlatformException

ChannelPlatformResponseInvalid

ChannelPlatformResponseInvalid

Error message

Failed to fetch permalink for ${objectId}

What it means

fetchPermalink in threads-publish.provider.ts calls the Threads Graph API 'permalink' field for a published media object. If the API call throws, or returns a post without a permalink, it logs a warning and throws ThreadsPlatformException.validation(ChannelPlatformResponseInvalid, PlatformUnavailable) — i.e., Threads did not return a usable permalink for the published post.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/threads/threads-publish.provider.ts:404

      } satisfies ThreadsPublishDataOption),
    }
  }

  private async fetchPermalink(
    objectId: string,
    accessToken: string,
  ): Promise<string> {
    try {
      const publishedPost = await this.threadsService.getPublishedPost(
        objectId,
        accessToken,
        'permalink',
      )
      if (publishedPost.permalink)
        return publishedPost.permalink
    }
    catch (err) {
      this.logger.warn(err, `Failed to fetch permalink for ${objectId}`)
    }
    throw ThreadsPlatformException.validation({
      code: ResponseCode.ChannelPlatformResponseInvalid,
      category: PlatformErrorCategory.PlatformUnavailable,
      context: {
        endpoint: 'fetchPermalink',
        platformWorkId: objectId,
      },
    })
  }

  private buildPostText(body: string | undefined): string {
    return (stripTopicsFromBody(body) ?? '').trim()
  }

  private buildPost(body: string | undefined): { text: string, topicTag?: string } {
    const topics = parseTopicsFromBody(body)
    return {

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Wait and retry: add a short delay/backoff after publish before requesting the permalink, since Threads may take time to expose it
  2. Verify the objectId is a published Threads media id and the token has threads_basic/read permissions
  3. Call the Graph API directly (curl with the same token) to confirm whether permalink is returned and inspect the raw error
  4. Treat the failure as non-fatal in the caller if a permalink is optional

Example fix

// before
if (publishedPost.permalink)
  return publishedPost.permalink
// after
if (publishedPost.permalink)
  return publishedPost.permalink
await new Promise(r => setTimeout(r, 3000)) // Threads may need time before permalink is available
const retried = await this.fetchPublishedPost(objectId, 'permalink')
if (retried?.permalink) return retried.permalink
Defensive patterns

Strategy: retry

Validate before calling

// before fetching permalink, confirm the media is published
const status = await fetch(`https://graph.threads.net/v1/${objectId}?fields=status&access_token=${token}`).then(r => r.json());
if (status?.status !== 'FINISHED') await delay(5000);

Type guard

function hasPermalink(p: unknown): p is { permalink: string } {
  return typeof p === 'object' && p !== null && 'permalink' in p && typeof (p as any).permalink === 'string' && (p as any).permalink.length > 0;
}

Try / catch

try {
  const url = await provider.permalink(objectId);
} catch (e) {
  // Threads may not expose permalink immediately: retry with backoff, then degrade gracefully
  await delay(3000);
  url = await provider.permalink(objectId).catch(() => null);
}

Prevention

When it happens

Trigger: permalink() is called shortly after publishing (media not yet finished processing server-side), the objectId is invalid or belongs to another container, the access token lacks permissions/is expired, or Threads returns an incomplete response without a permalink field.

Common situations: Querying permalink immediately after publish before Threads indexes the post; deleted/archived posts; token scope changes after a user re-authorizes with fewer permissions.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/e4da4b3b9a00a74a. Report an issue: GitHub.