Skip to content

DX-2949: bypass qstash v3 header prefixing when starting workflow runs - #44

Merged
CahidArda merged 4 commits into
masterfrom
DX-2949
Aug 21, 2026
Merged

DX-2949: bypass qstash v3 header prefixing when starting workflow runs#44
CahidArda merged 4 commits into
masterfrom
DX-2949

Conversation

@CahidArda

Copy link
Copy Markdown
Contributor

Summary

qstash-py >= 3.0 prefixes every header passed to publish_json with Upstash-Forward- (2.x left upstash-* headers alone). The workflow control headers (Upstash-Workflow-Init, -RunId, -Url, Upstash-Feature-Set, retries) were therefore delivered as plain forwarded headers, QStash never recognized the request as a workflow start, and it called the endpoint with the raw initial body instead of the step array. The SDK then crashed in _parse_payload with TypeError: string indices must be integers, not 'str' on every multi-step run (GitHub #40). With the bump to qstash ^3.4.0 this would have affected every 0.1.5 user.

This mirrors workflow-js: the first invocation is sent via /v2/batch (JS client.batch) and the context.call result via /v2/publish/<url> (JS publishJSON), both through the raw HTTP client with headers untouched — the same approach already used for step submission.

Changes

  • workflow_requests.py: new _get_first_invocation_batch_body (shared sync/async) and _get_redact_headers (builds Upstash-Redact-Fields ourselves); _trigger_first_invocation posts to /v2/batch via http.request
  • Third-party call result (sync + async) published via http.request to /v2/publish/<url> instead of publish_json
  • Content-Type taken from the user's request when present (else application/json); string payloads sent as-is, others JSON-serialized — as in workflow-js
  • Tests: test_context.py (sync/async) redact test now expects the batch request with un-prefixed control headers; test_redact.py rewritten to assert the Upstash-Redact-Fields value and that publish_json is never used

Testing

  • ruff format / ruff check / mypy clean; pytest tests → 12 passed
  • Reproduced the original failure end-to-end with npx @upstash/qstash-cli dev + a FastAPI app matching the reporter's setup on qstash-py 3.4.0 (and confirmed 0.1.3 + qstash 2.0.5 works / 0.1.3 + qstash 3.4.0 fails); after the fix, both steps complete

…s and submitting call results

qstash-py >=3 prefixes every header with Upstash-Forward-, which turned the
workflow control headers (Upstash-Workflow-Init, -RunId, -Url, ...) into plain
forwarded headers. QStash then never recognized the workflow start and called
the endpoint with the raw body, crashing the step parser with
"string indices must be integers, not 'str'" (GH #40).

Mirror workflow-js: send the first invocation via /v2/batch and the
context.call result via /v2/publish through the raw HTTP client, and build the
Upstash-Redact-Fields header ourselves.
@linear-code

linear-code Bot commented Aug 21, 2026

Copy link
Copy Markdown

DX-2949

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes multi-step workflow start failures with qstash-py >= 3.0 by bypassing publish_json (which prefixes headers with Upstash-Forward-) for workflow-start and call-result publishing, ensuring QStash receives workflow control headers unmodified.

Changes:

  • Start workflow runs via a raw http.request to /v2/batch with an explicit batch body that preserves workflow control headers.
  • Publish third-party context.call results via raw http.request to /v2/publish/<url> instead of publish_json, avoiding header corruption.
  • Update/redesign redact-related tests to assert Upstash-Redact-Fields header construction and confirm publish_json is not used.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
upstash_workflow/workflow_requests.py Adds batch-body/header helpers and switches first-invocation + call-result publishing to raw HTTP requests.
upstash_workflow/asyncio/workflow_requests.py Mirrors the sync changes for async workflow start and call-result publishing.
tests/test_redact.py Reworks tests to validate Upstash-Redact-Fields header composition and ensure publish_json is not called.
tests/test_context.py Updates sync context tests to expect workflow start via /v2/batch with unprefixed control headers.
tests/asyncio/test_context.py Updates async context tests to expect workflow start via /v2/batch with unprefixed control headers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Mirrors workflow-js triggerFirstInvocation which passes failureUrl to getHeaders.
Without it, a failure of the first step never reached the failure function.
context.call documented that a non-JSON body is returned as it is, but the
json.loads failure made it return the raw result dict instead of a
CallResponse, so resp.status/resp.body raised AttributeError.
@CahidArda
CahidArda merged commit 9fde1c6 into master Aug 21, 2026
1 check passed
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.

2 participants