Skip to content

recreateUserHeaders() does not filter Fastly CDN headers, causing 503 loop detection #183

@wfalowski

Description

@wfalowski

Summary

The Upstash Workflow SDK's recreateUserHeaders() function filters known CDN-specific headers (Cloudflare, Vercel, Render) before forwarding them via the Upstash-Forward-* mechanism. However, Fastly headers are not included in the exclusion list. When a workflow is hosted on Railway (which uses Fastly as its edge proxy since yesterday), Fastly tracking headers — most critically fastly-ff — are accumulated across workflow steps and eventually trigger Fastly's loop detection, returning a 503 error after ~4 steps.


Environment

  • SDK: @upstash/workflow
  • Hosting: Railway (Fastly edge proxy)
  • QStash callbacks: each step callback passes through Railway/Fastly
  • Triggered by: Railway introducing a Fastly CDN layer (previously worked fine)

Root Cause

recreateUserHeaders() forwards request headers to the next workflow step via Upstash-Forward-*. It filters some CDN headers but misses Fastly's:

fastly-ff
fastly-client
fastly-client-ip
fastly-ssl
fastly-temp-xff
cdn-loop
via

On each workflow step callback, Railway's Fastly edge appends a new entry to the existing Fastly-Ff header (rather than replacing it). Since the header is forwarded from step to step via Upstash-Forward-fastly-ff, the value grows with each step:

  • Step 1: Fastly-Ff: <pop1>
  • Step 2: Fastly-Ff: <pop1>, <pop2>
  • Step 3: Fastly-Ff: <pop1>, <pop2>, <pop3>
  • Step 4+: 503 — Pop visit count exceeded max threshold

This is why the failure is step-count dependent, not random — and why it only affects workflows with multiple steps.


Expected Behavior

recreateUserHeaders() should strip Fastly-specific headers the same way it currently strips Cloudflare/Vercel/Render headers, so they are never forwarded to subsequent steps via Upstash-Forward-*.


Suggested Fix

Add Fastly headers to the exclusion list in recreateUserHeaders():

const HEADERS_TO_STRIP = [
  // existing entries...

  // Fastly (Railway edge proxy)
  'fastly-ff',
  'fastly-client',
  'fastly-client-ip',
  'fastly-ssl',
  'fastly-temp-xff',
  'cdn-loop',
  'via',
];

Workaround (app-level)

For anyone hitting this today, strip the headers manually in your application before the request reaches the SDK:

// middleware.ts
const FASTLY_HEADERS_TO_STRIP = [
  'fastly-ff',
  'cdn-loop',
  'fastly-client',
  'fastly-client-ip',
  'fastly-ssl',
  'fastly-temp-xff',
  'via',
];

And optionally strip from the response too:

Impact

This affects all Railway users using Upstash Workflows with multi-step pipelines. The issue is silent until you have enough workflow steps to exceed Fastly's threshold — making it hard to diagnose without knowing the root cause.


Additional Context

We've reported this to Railway as well, suggesting they configure Fastly to strip/reset the Fastly-Ff header on incoming external requests. A fix in the SDK would be the most robust solution as it protects all Railway/Fastly users without requiring app-level workarounds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions