paperclipai/paperclip · error
CHAT_PROVIDER_PRETRANSPORT_REJECTED
CHAT_PROVIDER_PRETRANSPORT_REJECTED
Error message
Invalid Slack receipt destination
What it means
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.
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
Example fix
// before
await applySlackReceiptReaction({ operation: op, reaction, botToken: process.env.SLACK_BOT_TOKEN, ... });
// after
const botToken = process.env.SLACK_BOT_TOKEN?.trim();
if (botToken && !/[\r\n]/.test(botToken) && (op === 'add' || op === 'remove') && reaction === 'eyes') {
await applySlackReceiptReaction({ operation: op, reaction, botToken, ... });
} Defensive patterns
Strategy: validation
Validate before calling
const ok = (op === 'add' || op === 'remove') && reaction === 'eyes' && typeof botToken === 'string' && botToken.length > 0 && ![\r\n].some(c => botToken.includes(c));
Type guard
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)); } Try / catch
try { await applySlackReceiptReaction(input); } catch (err) { if ((err as { code?: string }).code === 'CHAT_PROVIDER_PRETRANSPORT_REJECTED') { fixInputAndRequeue(input); return; } throw err; } Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- CHAT_PROVIDER_PRETRANSPORT_REJECTED
- A full lowercase source SHA is required.
- A reusable lease cannot be replaced and reacquired in the…
- A reusable lease handoff requires an execution workspace…
- A safe, unique --revision is required
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/b6cc8298b0968bf4.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/chat-slack-receipts.ts:70
export async function applySlackReceiptReaction(
input: SlackReceiptMutation & { botToken: string },
fetchImpl: typeof globalThis.fetch = globalThis.fetch,
): Promise<void> {
const parts = input.threadId.split(":");
if (
parts[0] !== "slack" ||
!/^[CDG][A-Z0-9]+$/.test(parts[1] ?? "") ||
parts.length < 2 ||
parts.length > 3 ||
(parts[2] !== undefined && !/^\d{1,12}\.\d{1,6}$/.test(parts[2])) ||
!/^\d{1,12}\.\d{1,6}$/.test(input.messageId) ||
!["add", "remove"].includes(input.operation) ||
input.reaction !== "eyes" ||
!input.botToken ||
/[\r\n]/.test(input.botToken)
) {
throw Object.assign(new Error("Invalid Slack receipt destination"), {
code: "CHAT_PROVIDER_PRETRANSPORT_REJECTED",
});
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), SLACK_RECEIPT_TIMEOUT_MS);
let response: Response;
let body: Record<string, unknown> | null = null;
try {
response = await fetchImpl(
`https://slack.com/api/reactions.${input.operation}`,
{
method: "POST",
redirect: "error",
signal: controller.signal,
headers: {
authorization: `Bearer ${input.botToken}`,
"content-type": "application/json; charset=utf-8",
},
body: JSON.stringify({View on GitHub (pinned to 01ad858492)