feat(server): queued-only server-route dispatch (#23134)
## Context
Follow-up to the incident where 500s and latency spiked around 6pm until
the Recall webhook was disabled. Server routes
(`/webhooks/server/:resolverUid`) ran the resolver **and** the target
logic function synchronously inside the API request, so any handler
throw became a 500 and any slowdown past Svix's delivery timeout marked
the delivery failed — Svix redelivered, feeding load back into the API
in a self-sustaining storm.
## What changed
- Every server-route request acks with **202 `{ queued: true }`** as
soon as the resolver returns; the target runs on `logicFunctionQueue`.
Signature verification stays synchronous in the resolver and still
rejects with a non-2xx. External senders never observe target latency or
failures.
- The resolver contract is unchanged from main: `{ workspaceId,
targetLogicFunctionUniversalIdentifier, payload? }`.
- The target lookup still happens synchronously before enqueueing,
scoped to the resolver's application registration, so unknown targets
404 as before.
- Endpoints whose caller must read the response body (challenge
handshakes, Slack commands) should use `httpRouteTriggerSettings`
routes.
Final diff is 4 files: the server-route service, its spec, the
integration spec, and the docs page. Trigger jobs, fan-out, message
queue, shared types, and SDK are all untouched.
## Tests
- `server-route-trigger.service.spec.ts`: 202 ack + enqueue,
unknown-target 404 without enqueue, resolver auth/contract/error
mapping.
- Integration: `server-route-trigger-authorization.integration-spec.ts`
asserts the 202 queued ack (run locally against a seeded DB, green).
- `typecheck` + `lint:diff-with-main` + `oxfmt` clean.
## Notes
- **Breaking for existing server-route resolvers**: responses are always
202; the target's return value no longer reaches the caller. Existing
resolvers returning response bodies must move those endpoints to
`httpRouteTriggerSettings`.
- A queued target's handler failure is recorded in execution logs but
not retried (same as other queue-executed functions today); retry
semantics are deliberately out of scope here.
- Follow-up candidates: retry-on-failure semantics for queued
executions, `addBulk` for single-round-trip fan-out, declarative
signature verification to take resolver code out of the request path,
moving the call-recorder 250s artifacts import off the API request path.
- Companion PR #23135 (call-recorder): no app change needed for dispatch
— queued dispatch applies by default.
This commit is contained in:
@@ -59,7 +59,7 @@ To invoke a route-triggered logic function from a (headless) front component, se
|
||||
- **cron**: Runs your function on a schedule using a CRON expression.
|
||||
- **databaseEvent**: Runs on workspace object lifecycle events. When the event operation is `updated`, specific fields to listen to can be specified in the `updatedFields` array. If left undefined or empty, any update will trigger the function.
|
||||
> e.g. `person.updated`, `*.created`, `company.*`
|
||||
- **serverRoute**: Exposes a single registration-scoped HTTP route. A **resolver** function (declared with `serverRouteTriggerSettings`) runs in the owner workspace and returns the target workspace AND the target logic function to dispatch to; the platform then runs that **target** function and returns its response. See [Server route trigger](#server-route-trigger).
|
||||
- **serverRoute**: Exposes a single registration-scoped HTTP route. A **resolver** function (declared with `serverRouteTriggerSettings`) runs in the owner workspace and returns the target workspace AND the target logic function to dispatch to; the platform acks with `202` and runs that **target** function on the worker queue. See [Server route trigger](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
You can also manually execute a function using the CLI:
|
||||
@@ -290,7 +290,7 @@ For request signatures, most providers sign with HMAC-SHA256; the parts that dif
|
||||
The resolver example above already shows the GitHub HMAC-SHA256 flow — adapt the header name, digest encoding, and signed-payload string per the provider you're integrating.
|
||||
|
||||
<Note>
|
||||
The target runs **synchronously** and its returned value becomes the HTTP response, so callers see your status code and can retry on non-2xx. Keep both handlers fast — some providers (e.g. Slack) time out in a few seconds. Because the resolver is reachable as a public endpoint, protect it with rate limiting at your edge.
|
||||
The route responds `202 { queued: true }` right after the resolver returns and the target runs on the worker queue — the caller never observes the target's latency, result, or failures (those are recorded in execution logs). This keeps sender redeliveries from amplifying processing slowdowns, which is what you want for webhook ingestion. For endpoints whose caller must read the response body (challenge handshakes, Slack commands), use an `httpRouteTriggerSettings` route instead. Keep the resolver fast — some providers (e.g. Slack) time out in a few seconds. Because the resolver is reachable as a public endpoint, protect it with rate limiting at your edge.
|
||||
</Note>
|
||||
|
||||
#### Database event trigger payload
|
||||
|
||||
Reference in New Issue
Block a user