Tencent/WeKnora · error

no file URL in message

Error message

no file URL in message

What it means

DownloadFile downloads and decrypts the media attached to a WeCom aibot message. Each incoming message carries an encrypted URL in FileKey; when that field is empty there is nothing to download, so the adapter returns this error. It guards against messages that have no file payload (e.g. plain text) or were parsed incompletely.

Source

Thrown at internal/im/wecom/ws_adapter.go:85

func (a *WSAdapter) FinalizeStream(ctx context.Context, incoming *im.IncomingMessage, streamID string, finalContent string) error {
	return a.client.FinalizeStream(ctx, incoming, streamID, finalContent)
}

func (a *WSAdapter) SendStreamChunk(ctx context.Context, incoming *im.IncomingMessage, streamID string, content string) error {
	return a.client.UpdateStreamContent(ctx, incoming, streamID, content)
}

func (a *WSAdapter) EndStream(ctx context.Context, incoming *im.IncomingMessage, streamID string) error {
	return a.client.EndStream(ctx, incoming, streamID)
}

// ── FileDownloader implementation ──
// WeCom aibot provides AES-256-CBC encrypted URLs for image/file/video messages.
// Each message carries its own aeskey for decryption.

func (a *WSAdapter) DownloadFile(ctx context.Context, msg *im.IncomingMessage) (io.ReadCloser, string, error) {
	if msg.FileKey == "" {
		return nil, "", fmt.Errorf("no file URL in message")
	}
	fileName := msg.FileName
	if fileName == "" {
		fileName = msg.FileKey
	}

	// Download the (encrypted) file content
	reader, fileName, err := downloadFromURL(ctx, msg.FileKey, fileName, a.client.extraAllowedHost)
	if err != nil {
		return nil, "", err
	}

	// If an AES key is provided, the downloaded content is AES-256-CBC encrypted
	// and must be decrypted before use. This is the case for WeCom aibot long
	// connection mode where each file/image message carries a per-message aeskey.
	aesKeyB64 := msg.Extra["aes_key"]
	if aesKeyB64 == "" {
		// No encryption — return raw content (e.g. webhook mode uses media API)

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Check msg.FileKey (or the message type) is non-empty before calling DownloadFile.
  2. Skip download for non-media message types (text, event, etc.).
  3. Log the raw message payload to confirm the parser populated FileKey; if media messages consistently lack it, upgrade/fix the callback parsing code.

Example fix

// before
rc, name, err := adapter.DownloadFile(ctx, msg)
// after
if msg.FileKey == "" {
    return nil, nil // not a media message
}
rc, name, err := adapter.DownloadFile(ctx, msg)
Defensive patterns

Strategy: validation

Validate before calling

if msg.FileKey == "" {
    // not a media message; skip download
    return nil
}
rc, name, err := adapter.DownloadFile(ctx, msg)

Type guard

func hasDownloadableFile(msg *im.IncomingMessage) bool {
    return msg != nil && msg.FileKey != ""
}

Try / catch

rc, name, err := adapter.DownloadFile(ctx, msg)
if err != nil && strings.Contains(err.Error(), "no file URL in message") {
    return nil, nil // expected for text messages
}

Prevention

When it happens

Trigger: Calling DownloadFile with an *im.IncomingMessage whose FileKey is empty — typically a text message, or a media message whose parse step failed to populate FileKey.

Common situations: Downloading attachments for every incoming message without checking message type first; upstream parser version mismatch leaving FileKey unset; voice/emoji messages that carry no downloadable URL.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/47e92a03358b6982. Report an issue: GitHub.