Conversation
…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.
There was a problem hiding this comment.
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.requestto/v2/batchwith an explicit batch body that preserves workflow control headers. - Publish third-party
context.callresults via rawhttp.requestto/v2/publish/<url>instead ofpublish_json, avoiding header corruption. - Update/redesign redact-related tests to assert
Upstash-Redact-Fieldsheader construction and confirmpublish_jsonis 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
qstash-py >= 3.0 prefixes every header passed to
publish_jsonwithUpstash-Forward-(2.x leftupstash-*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_payloadwithTypeError: string indices must be integers, not 'str'on every multi-step run (GitHub #40). With the bump toqstash ^3.4.0this would have affected every 0.1.5 user.This mirrors workflow-js: the first invocation is sent via
/v2/batch(JSclient.batch) and thecontext.callresult via/v2/publish/<url>(JSpublishJSON), 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(buildsUpstash-Redact-Fieldsourselves);_trigger_first_invocationposts to/v2/batchviahttp.requesthttp.requestto/v2/publish/<url>instead ofpublish_jsonapplication/json); string payloads sent as-is, others JSON-serialized — as in workflow-jstest_context.py(sync/async) redact test now expects the batch request with un-prefixed control headers;test_redact.pyrewritten to assert theUpstash-Redact-Fieldsvalue and thatpublish_jsonis never usedTesting
ruff format/ruff check/mypyclean;pytest tests→ 12 passednpx @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