Skip to content

fix(slack): add missing QStash endpoint for DM messages#1411

Merged
saddlepaddle merged 1 commit into
mainfrom
slack-chat
Feb 12, 2026
Merged

fix(slack): add missing QStash endpoint for DM messages#1411
saddlepaddle merged 1 commit into
mainfrom
slack-chat

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 12, 2026

Summary

  • Adds the missing jobs/process-assistant-message/route.ts QStash handler — the event router was dispatching DM messages to this endpoint, but no route existed (404)
  • Adds resolveUserMentions to the DM handler to match the mention handler pattern

Test plan

  • Send a DM to the Slack bot, verify it responds
  • Reply in the DM thread, verify the agent sees prior thread context
  • Send a DM with an @mention, verify the user's display name appears in the agent's prompt

Summary by CodeRabbit

  • Bug Fixes

    • Slack messages containing user mentions are now properly resolved before processing by the assistant, improving accuracy of responses in conversations with user references.
  • New Features

    • Enhanced Slack integration with improved message processing validation and security verification for more reliable request handling.

The event router was dispatching DM messages to
/api/integrations/slack/jobs/process-assistant-message via QStash,
but no route handler existed — every DM job hit a 404.

Also adds resolveUserMentions to the DM handler to match the
mention handler, so <@u12345> refs get resolved to display names.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

The PR adds user mention resolution preprocessing to Slack message handling and introduces a new server route that validates Slack webhook requests via QStash signature verification before triggering message processing.

Changes

Cohort / File(s) Summary
Slack Message Processing Enhancement
apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts
Added resolveUserMentions preprocessing step to convert user mentions in message text before passing to the agent, with the resolved text replacing the raw event text in the prompt.
QStash Route Handler
apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts
New POST route handler that validates incoming requests using QStash Receiver signature verification, parses payload with zod schema validation, and invokes processAssistantMessage with validated event data. Returns 401 for invalid signatures, 400 for invalid payloads, and 200 on success.

Sequence Diagram(s)

sequenceDiagram
    actor Client as HTTP Client
    participant Route as POST Route Handler
    participant QStash as QStash Receiver
    participant Processor as Process Assistant<br/>Message
    participant Slack as Slack Agent
    
    Client->>Route: POST /slack/jobs/process-assistant-message
    Route->>QStash: Create Receiver & Validate Signature
    alt Invalid Signature
        QStash-->>Route: Signature Invalid
        Route-->>Client: 401 Unauthorized
    else Valid Signature
        QStash-->>Route: Signature Valid
        Route->>Route: Parse & Validate Payload (Zod)
        alt Invalid Payload
            Route-->>Client: 400 Bad Request
        else Valid Payload
            Route->>Processor: Invoke with event, teamId, eventId
            Processor->>Processor: resolveUserMentions(event.text)
            Processor->>Slack: runSlackAgent(resolvedText, ...)
            Slack-->>Processor: Response
            Processor-->>Route: Complete
            Route-->>Client: { success: true }
        end
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • superset-sh/superset#1409: Introduces and exports the resolveUserMentions helper utility that this PR consumes in process-assistant-message.
  • superset-sh/superset#1101: Modifies the same process-assistant-message.ts file to handle message posting, progress tracking, and side-effects in the Slack agent workflow.

Poem

🐰 A mention resolves with a whispered pass,
QStash keys validate the request en masse,
From webhook to agent, the message takes flight,
User names decoded—oh what a delight!
Slack integration blooms, pixel-perfect and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a missing QStash endpoint for handling DM messages in Slack integration.
Description check ✅ Passed The description covers the main objectives (missing route and resolveUserMentions addition) and includes a test plan, but lacks sections for Related Issues, Type of Change, and detailed Testing information as specified in the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch slack-chat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts`:
- Line 45: The code calls payloadSchema.safeParse(JSON.parse(body)) which lets
JSON.parse throw on malformed input; wrap the JSON.parse in a try-catch (or use
a safe JSON parse helper) before passing the result to payloadSchema.safeParse
and handle the parse error by returning a 400/invalid-body response and logging
the error; update the code around payloadSchema.safeParse, JSON.parse and body
in route.ts to first attempt a safe parse, then call
payloadSchema.safeParse(parsedBody).
- Around line 27-61: The POST handler should skip QStash signature verification
in development: introduce isDev (e.g., const isDev = env.NODE_ENV ===
"development") at the top of POST and wrap the existing signature checks and the
receiver.verify(...) call (and its signature presence check) in if (!isDev) {
... } so that in development the route proceeds without requiring
upstash-signature or calling receiver.verify, while keeping the payloadSchema
parsing and processAssistantMessage(...) call unchanged.
- Around line 35-43: receiver.verify throws a SignatureError on invalid
signatures so the existing isValid check is unreachable; wrap the call to
receiver.verify(...) in a try/catch inside route.ts (the handler around
receiver.verify), catch SignatureError (or any verification error) and return
Response.json({ error: "Invalid signature" }, { status: 401 }) from the catch
block to prevent a 500, otherwise proceed when verification succeeds.
🧹 Nitpick comments (2)
apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts (1)

67-73: Mention resolution integration looks correct.

The two-phase pattern (batch-fetch user info, then apply resolution) mirrors the existing mention handler. One minor observation: event.text ?? "" is evaluated twice — once to seed the resolver and once to apply it. Consider extracting to a local constant to avoid repetition.

♻️ Optional: extract raw text to a local variable
+	const rawText = event.text ?? "";
 	const resolve = await resolveUserMentions({
-		texts: [event.text ?? ""],
+		texts: [rawText],
 		slack,
 	});

 	const result = await runSlackAgent({
-		prompt: resolve(event.text ?? ""),
+		prompt: resolve(rawText),
 		channelId: event.channel,
apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts (1)

54-58: Unhandled rejection from processAssistantMessage would crash the route.

If processAssistantMessage throws, the POST handler has no try-catch, so it will return a 500 to QStash. QStash will then retry the message, which could cause duplicate "Thinking…" messages and repeated failures. Consider wrapping in a try-catch to log the error and return a controlled response (or let it propagate intentionally if you want QStash retries — in which case, add a comment explaining that).

♻️ Proposed: wrap in try-catch with logging
-	await processAssistantMessage({
-		event: { ...parsed.data.event, subtype: undefined },
-		teamId: parsed.data.teamId,
-		eventId: parsed.data.eventId,
-	});
-
-	return Response.json({ success: true });
+	try {
+		await processAssistantMessage({
+			event: { ...parsed.data.event, subtype: undefined },
+			teamId: parsed.data.teamId,
+			eventId: parsed.data.eventId,
+		});
+		return Response.json({ success: true });
+	} catch (err) {
+		console.error("[slack/process-assistant-message] Processing failed:", err);
+		return Response.json({ error: "Processing failed" }, { status: 500 });
+	}

Comment on lines +27 to +61
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("upstash-signature");

if (!signature) {
return Response.json({ error: "Missing signature" }, { status: 401 });
}

const isValid = await receiver.verify({
body,
signature,
url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
});

if (!isValid) {
return Response.json({ error: "Invalid signature" }, { status: 401 });
}

const parsed = payloadSchema.safeParse(JSON.parse(body));
if (!parsed.success) {
console.error(
"[slack/process-assistant-message] Invalid payload:",
parsed.error,
);
return Response.json({ error: "Invalid payload" }, { status: 400 });
}

await processAssistantMessage({
event: { ...parsed.data.event, subtype: undefined },
teamId: parsed.data.teamId,
eventId: parsed.data.eventId,
});

return Response.json({ success: true });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all route.ts files under jobs directories to identify QStash job handlers
echo "=== QStash Job Routes ==="
find . -path "*/jobs/*/route.ts" -type f | head -20

echo -e "\n=== Content of similar job routes ==="
find . -path "*/jobs/*/route.ts" -type f -exec sh -c 'echo "File: $1"; head -80 "$1"; echo "---"' _ {} \;

Repository: superset-sh/superset

Length of output: 11813


🏁 Script executed:

#!/bin/bash
# Search for receiver.verify patterns to see how signature verification is done elsewhere
echo "=== Receiver.verify usages ==="
rg -n 'receiver\.verify' --type ts --type tsx -B2 -A5

Repository: superset-sh/superset

Length of output: 139


🏁 Script executed:

#!/bin/bash
# Also search for other patterns of Upstash/QStash integration patterns
echo "=== QStash/Upstash patterns ==="
rg -n 'upstash|QStash|Receiver' --type ts --type tsx -l | head -20

Repository: superset-sh/superset

Length of output: 140


Add development environment check to skip signature verification in development.

This route lacks the development environment check present in other QStash job handlers (github/initial-sync, linear/initial-sync). In development, QStash cannot reach localhost, making signature verification impossible. Wrap the receiver.verify() call in an isDev check:

const isDev = env.NODE_ENV === "development";

if (!isDev) {
  if (!signature) {
    return Response.json({ error: "Missing signature" }, { status: 401 });
  }

  const isValid = await receiver.verify({
    body,
    signature,
    url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
  });

  if (!isValid) {
    return Response.json({ error: "Invalid signature" }, { status: 401 });
  }
}
🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts`
around lines 27 - 61, The POST handler should skip QStash signature verification
in development: introduce isDev (e.g., const isDev = env.NODE_ENV ===
"development") at the top of POST and wrap the existing signature checks and the
receiver.verify(...) call (and its signature presence check) in if (!isDev) {
... } so that in development the route proceeds without requiring
upstash-signature or calling receiver.verify, while keeping the payloadSchema
parsing and processAssistantMessage(...) call unchanged.

Comment on lines +35 to +43
const isValid = await receiver.verify({
body,
signature,
url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
});

if (!isValid) {
return Response.json({ error: "Invalid signature" }, { status: 401 });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@upstash/qstash Receiver verify method throws invalid signature documentation

💡 Result:

The Upstash QStash docs indicate Receiver.verify(...) will fail (often surfacing as “Invalid signature”) when any input used for verification doesn’t exactly match what QStash signed, especially:

  • You didn’t use the raw request body: many frameworks parse JSON into an object; re-stringifying it (e.g. JSON.stringify(req.body)) can change spacing/key order and cause verification to fail. The docs explicitly warn to use the raw body string “as is.” [1]
  • The url you pass doesn’t exactly match the JWT sub claim (scheme/host/path). The manual verification example shows the verifier checks p.sub !== url and throws (subject mismatch). So passing only a host, missing https://, missing path, or using a different public URL than QStash is calling will fail verification. [2]
  • Wrong signing keys (or keys were rolled and your app didn’t update). QStash uses “current” and “next” signing keys; your receiver should verify with both. [1]

Helpful cross-check: QStash delivers the body unchanged (“passed as is”), so your handler must read it in that same raw form for hashing/verification. [3]

Sources: Upstash “Verify Signatures” doc (raw body + Receiver usage) [1], Upstash AWS Lambda quickstart (shows sub/url and body-hash checks) [2], Upstash “Receiving Messages” (body passed as-is) [3].


🌐 Web query:

@upstash/qstash Receiver.verify() return type throws error documentation examples

💡 Result:

In @upstash/qstash (TypeScript), Receiver.verify(...) is async and effectively works like:

  • Return type: Promise<boolean> (it resolves to true when verification succeeds).
  • Failure mode: it throws a SignatureError (it does not return false for invalid signatures). The SDK throws on JWT verification errors, subject (url) mismatch, or body-hash mismatch. [1]

So usage patterns in docs/examples differ, but both are valid:

Pattern A: try/catch (recommended, since failures throw)

try {
  await receiver.verify({ signature, body, url });
  // verified
} catch (err) {
  // invalid signature (SignatureError)
}

Example shown in Upstash’s AWS Lambda quickstart. [2]

Pattern B: assign to isValid (works, but false is not expected)

const isValid = await receiver.verify({ signature, body, url });
if (!isValid) {
  // typically unreachable; invalid cases throw instead
}

Shown in Upstash docs and quickstarts. [3]

Docs/examples: Receiver verify examples are in the “Verify Signatures” doc and the Receiver SDK example page. [3]

Sources:
[1] UNPKG compiled source for @upstash/qstash showing SignatureError and verify() implementation/behavior.
[2] Upstash AWS Lambda (Node) quickstart demonstrating try/catch around verify().
[3] Upstash docs pages showing const isValid = await receiver.verify(...) usage.


receiver.verify() throws on invalid signatures — the isValid check is unreachable.

The QStash Receiver.verify() method throws a SignatureError when the signature is invalid rather than returning false. This means the if (!isValid) block is dead code, and an invalid signature will produce an unhandled exception (500 error) instead of the intended 401 response. Wrap the verify call in a try-catch to properly handle verification failures.

Proposed fix
-	const isValid = await receiver.verify({
-		body,
-		signature,
-		url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
-	});
-
-	if (!isValid) {
-		return Response.json({ error: "Invalid signature" }, { status: 401 });
-	}
+	try {
+		await receiver.verify({
+			body,
+			signature,
+			url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
+		});
+	} catch (err) {
+		console.error("[slack/process-assistant-message] Invalid QStash signature:", err);
+		return Response.json({ error: "Invalid signature" }, { status: 401 });
+	}
🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts`
around lines 35 - 43, receiver.verify throws a SignatureError on invalid
signatures so the existing isValid check is unreachable; wrap the call to
receiver.verify(...) in a try/catch inside route.ts (the handler around
receiver.verify), catch SignatureError (or any verification error) and return
Response.json({ error: "Invalid signature" }, { status: 401 }) from the catch
block to prevent a 500, otherwise proceed when verification succeeds.

return Response.json({ error: "Invalid signature" }, { status: 401 });
}

const parsed = payloadSchema.safeParse(JSON.parse(body));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JSON.parse(body) can throw on malformed input, resulting in an unhandled 500.

If QStash delivers a corrupted or non-JSON body that passes signature verification, JSON.parse will throw before the Zod validation can catch the issue. Wrap it or use a safe parse.

🛡️ Proposed fix — combine with the Zod parse in a single try-catch
-	const parsed = payloadSchema.safeParse(JSON.parse(body));
-	if (!parsed.success) {
+	let jsonBody: unknown;
+	try {
+		jsonBody = JSON.parse(body);
+	} catch {
+		return Response.json({ error: "Invalid JSON" }, { status: 400 });
+	}
+
+	const parsed = payloadSchema.safeParse(jsonBody);
+	if (!parsed.success) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsed = payloadSchema.safeParse(JSON.parse(body));
let jsonBody: unknown;
try {
jsonBody = JSON.parse(body);
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const parsed = payloadSchema.safeParse(jsonBody);
if (!parsed.success) {
🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts`
at line 45, The code calls payloadSchema.safeParse(JSON.parse(body)) which lets
JSON.parse throw on malformed input; wrap the JSON.parse in a try-catch (or use
a safe JSON parse helper) before passing the result to payloadSchema.safeParse
and handle the parse error by returning a 400/invalid-body response and logging
the error; update the code around payloadSchema.safeParse, JSON.parse and body
in route.ts to first attempt a safe parse, then call
payloadSchema.safeParse(parsedBody).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 12, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app
  • ✅ Streams Fly.io app

Thank you for your contribution! 🎉

@saddlepaddle saddlepaddle merged commit 4fd95ab into main Feb 12, 2026
15 checks passed
@Kitenite Kitenite deleted the slack-chat branch February 12, 2026 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant