yikart/AiToEarn · error

Relay uploadSign returned no uploadUrl: ${JSON.stringify(sig

Error message

Relay uploadSign returned no uploadUrl: ${JSON.stringify(signResult)}

What it means

uploadFileFromLocalUrl asks the Relay service to sign an asset upload via relayMediaService.uploadSign and then PUTs the file to the returned presigned URL. If the sign response contains no uploadUrl (missing field, null, or an unexpected error payload), the code cannot proceed and throws with the full signResult serialized for debugging.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/relay-media/relay-media-resolver.service.ts:154

  private isCommonResponse<T>(body: RelayCommonResponse<T> | T): body is RelayCommonResponse<T> {
    return typeof body === 'object' && body !== null && 'data' in body
  }

  private async uploadFileFromLocalUrl(localUrl: string): Promise<string> {
    const filename = basename(new URL(localUrl).pathname)

    const fileResponse = await axios.get(localUrl, { responseType: 'arraybuffer' })
    const contentType = fileResponse.headers['content-type'] || 'application/octet-stream'
    const size = (fileResponse.data as ArrayBuffer).byteLength

    const signResult = await this.post<UploadSignResult>('/api/assets/uploadSign', {
      filename,
      type: AssetType.Temp,
      size,
    })

    if (!signResult.uploadUrl) {
      throw new Error(`Relay uploadSign returned no uploadUrl: ${JSON.stringify(signResult)}`)
    }

    await axios.put(signResult.uploadUrl, fileResponse.data, {
      headers: { 'Content-Type': contentType },
      timeout: this.config?.timeout,
    })

    await this.post(`/api/assets/${signResult.id}/confirm`, {})
    this.logger.debug({ localUrl, relayUrl: signResult.url }, 'Uploaded local media to relay')
    return signResult.url
  }

  private escapeRegExp(value: string): string {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  }
}

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Inspect the JSON in the message: it contains the full signResult and usually reveals the real Relay error (e.g. {code, message}).
  2. Verify Relay credentials (RELAY_API_KEY) match the environment: China keys with *.aitoearn.cn URLs, international keys with *.aitoearn.ai URLs — mismatches often yield 401 bodies without uploadUrl.
  3. Check the Relay service's uploadSign endpoint health/logs; confirm storage presigning is configured.
  4. Confirm the Relay client contract (uploadSign response type) matches the deployed Relay version; upgrade or pin versions.
  5. Add a fallback: surface the signResult error to the caller instead of silently failing the whole text resolution.

Example fix

// before
if (!signResult.uploadUrl) {
  throw new Error(`Relay uploadSign returned no uploadUrl: ${JSON.stringify(signResult)}`)
}
// after
if (!signResult.uploadUrl) {
  this.logger.error(`Relay uploadSign failed: ${JSON.stringify(signResult)}`)
  throw new AppException(ResponseCode.RelayUploadSignFailed)
}
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await relay.uploadSign({ filename, type, size })
if (!res || typeof res.uploadUrl !== 'string' || res.uploadUrl.length === 0) {
  throw new Error(`uploadSign failed: ${JSON.stringify(res)}`)
}

Type guard

function hasUploadUrl(r: unknown): r is { uploadUrl: string } {
  return !!r && typeof r === 'object' && 'uploadUrl' in r && typeof (r as any).uploadUrl === 'string' && (r as any).uploadUrl.length > 0
}

Try / catch

try {
  const url = await uploadFileFromLocalUrl(...)
} catch (err) {
  if (err.message.includes('no uploadUrl')) {
    logger.error('Relay sign failed', err.message) // inspect embedded signResult JSON
    // verify Relay credentials/env pairing, then retry or surface a friendly error
  }
  throw err
}

Prevention

When it happens

Trigger: uploadFileFromLocalUrl (invoked via resolveText) receives a signResult object without an uploadUrl property — e.g. the Relay backend returned an error envelope, an auth failure body, or a schema change removed/renamed uploadUrl.

Common situations: Relay server deployed with an older/newer uploadSign contract; expired or mismatched API credentials causing a silent error body instead of an HTTP error; network proxy stripping the field; Relay storage backend (S3/OSS presigning) misconfigured so signing fails but the endpoint still returns 200.

Related errors


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