{"record":{"id":"b6cc8298b0968bf4","repo":"paperclipai/paperclip","slug":"chat-provider-pretransport-rejected-b6cc82","errorCode":"CHAT_PROVIDER_PRETRANSPORT_REJECTED","errorMessage":"Invalid Slack receipt destination","messagePattern":"Invalid Slack receipt destination","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/chat-slack-receipts.ts","lineNumber":70,"sourceCode":"export async function applySlackReceiptReaction(\n  input: SlackReceiptMutation & { botToken: string },\n  fetchImpl: typeof globalThis.fetch = globalThis.fetch,\n): Promise<void> {\n  const parts = input.threadId.split(\":\");\n  if (\n    parts[0] !== \"slack\" ||\n    !/^[CDG][A-Z0-9]+$/.test(parts[1] ?? \"\") ||\n    parts.length < 2 ||\n    parts.length > 3 ||\n    (parts[2] !== undefined && !/^\\d{1,12}\\.\\d{1,6}$/.test(parts[2])) ||\n    !/^\\d{1,12}\\.\\d{1,6}$/.test(input.messageId) ||\n    ![\"add\", \"remove\"].includes(input.operation) ||\n    input.reaction !== \"eyes\" ||\n    !input.botToken ||\n    /[\\r\\n]/.test(input.botToken)\n  ) {\n    throw Object.assign(new Error(\"Invalid Slack receipt destination\"), {\n      code: \"CHAT_PROVIDER_PRETRANSPORT_REJECTED\",\n    });\n  }\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), SLACK_RECEIPT_TIMEOUT_MS);\n  let response: Response;\n  let body: Record<string, unknown> | null = null;\n  try {\n    response = await fetchImpl(\n      `https://slack.com/api/reactions.${input.operation}`,\n      {\n        method: \"POST\",\n        redirect: \"error\",\n        signal: controller.signal,\n        headers: {\n          authorization: `Bearer ${input.botToken}`,\n          \"content-type\": \"application/json; charset=utf-8\",\n        },\n        body: JSON.stringify({","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/chat-slack-receipts.ts#L52-L88","documentation":"applySlackReceiptReaction performs strict pre-transport validation of the receipt input: operation must be 'add' or 'remove', reaction must be 'eyes', a botToken must be present and free of CR/LF characters, and the destination must be well-formed. Any violation throws before any HTTP call, tagged CHAT_PROVIDER_PRETRANSPORT_REJECTED. The CR/LF check prevents header injection into the Slack API request.","triggerScenarios":"Calling with operation other than add/remove; a reaction name other than 'eyes'; a missing/empty botToken; a botToken containing \\r or \\n (e.g. pasted multiline secret or secret containing a trailing newline from env/file read).","commonSituations":"Slack bot token loaded from a file/env with a trailing newline; wiring the wrong reaction name constant; misrouted handler passing a Teams-style operation value; missing Slack credentials in an environment.","solutions":["Trim/normalize the botToken when loading (strip trailing newlines) and verify it starts with xoxb-","Ensure only operation 'add'|'remove' and reaction 'eyes' are passed; fix the calling code's constants","Verify Slack credentials are configured in the environment before invoking"],"exampleFix":"// before\nawait applySlackReceiptReaction({ operation: op, reaction, botToken: process.env.SLACK_BOT_TOKEN, ... });\n// after\nconst botToken = process.env.SLACK_BOT_TOKEN?.trim();\nif (botToken && !/[\\r\\n]/.test(botToken) && (op === 'add' || op === 'remove') && reaction === 'eyes') {\n  await applySlackReceiptReaction({ operation: op, reaction, botToken, ... });\n}","handlingStrategy":"validation","validationCode":"const ok = (op === 'add' || op === 'remove') && reaction === 'eyes' && typeof botToken === 'string' && botToken.length > 0 && ![\\r\\n].some(c => botToken.includes(c));","typeGuard":"function isValidSlackReceiptInput(i: unknown): i is SlackReceiptInput { const x = i as SlackReceiptInput; return ['add','remove'].includes(x.operation) && x.reaction === 'eyes' && typeof x.botToken === 'string' && x.botToken.length > 0 && ![\\r\\n].some(c => x.botToken.includes(c)); }","tryCatchPattern":"try { await applySlackReceiptReaction(input); } catch (err) { if ((err as { code?: string }).code === 'CHAT_PROVIDER_PRETRANSPORT_REJECTED') { fixInputAndRequeue(input); return; } throw err; }","preventionTips":["Trim tokens read from env/files to strip trailing newlines","Centralize Slack constants (reaction 'eyes', operations) so callers can't pass wrong values","Validate destination/botToken at ingestion, before queueing the reaction"],"tags":["slack","validation","pretransport-rejection","header-injection"],"backgroundTag":"invalid-argument-value","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}