Skip to content

Commit 68f6844

Browse files
rubysclaude
andcommitted
Fix cross-app fly-replay: remove timeout/fallback that caused PDF 503s
When fly-replaying to a different app (e.g., smooth-pdf for PDF generation), the timeout and fallback fields were causing Fly to cut off requests after 30s and return them to Navigator as failures, which served 503 maintenance pages. This was a regression from adding timeout/fallback for same-app cross-region replays. Cross-app replays should let Fly handle the connection natively - the target app (smooth-pdf) needs time to cold start and generate PDFs. Without timeout and fallback, Fly waits for the target to respond without artificial cutoff. Also removes transform headers from cross-app replays since they're unnecessary (no loop detection needed) and their presence causes Fly to drop the original Authorization header. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 15ef20e commit 68f6844

3 files changed

Lines changed: 50 additions & 59 deletions

File tree

internal/server/fly_replay.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,32 +109,25 @@ func HandleFlyReplay(w http.ResponseWriter, r *http.Request, target string, stat
109109
responseMap = map[string]interface{}{
110110
"app": appName,
111111
"instance": machineID,
112-
"timeout": DefaultFlyReplayTimeout,
113-
"fallback": DefaultFlyReplayFallback,
114112
}
115113

116-
// Only add transform if staying within the same app
114+
// Only add timeout/fallback/transform for same-app replays
117115
if currentAppName == appName {
116+
responseMap["timeout"] = DefaultFlyReplayTimeout
117+
responseMap["fallback"] = DefaultFlyReplayFallback
118118
responseMap["transform"] = map[string]interface{}{
119119
"set_headers": buildTransformHeaders(),
120120
}
121121
}
122122
}
123123
} else if strings.HasPrefix(target, "app=") {
124-
// App-based fly-replay
124+
// App-based fly-replay to a different app (e.g., smooth-pdf)
125+
// No timeout/fallback - let Fly handle the connection natively,
126+
// allowing cold starts to complete without artificial cutoff
125127
appName := strings.TrimPrefix(target, "app=")
126128

127129
responseMap = map[string]interface{}{
128-
"app": appName,
129-
"timeout": DefaultFlyReplayTimeout,
130-
"fallback": DefaultFlyReplayFallback,
131-
}
132-
133-
// Only add transform if staying within the same app
134-
if currentAppName == appName {
135-
responseMap["transform"] = map[string]interface{}{
136-
"set_headers": buildTransformHeaders(),
137-
}
130+
"app": appName,
138131
}
139132
} else {
140133
// Region-based fly-replay (same app, different region)

internal/server/fly_replay_integration_test.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,22 @@ func TestFlyReplayIntegration_EndToEnd(t *testing.T) {
135135
}
136136
}
137137

138-
// Verify timeout and fallback are present
139-
if _, ok := response["timeout"]; !ok {
140-
t.Error("Response should contain 'timeout' field")
141-
}
142-
if _, ok := response["fallback"]; !ok {
143-
t.Error("Response should contain 'fallback' field")
138+
// Cross-app replays should NOT have timeout/fallback
139+
// Same-app (region) replays SHOULD have timeout/fallback
140+
if tt.expectedApp != "" {
141+
if _, ok := response["timeout"]; ok {
142+
t.Error("Cross-app replay should not contain 'timeout' field")
143+
}
144+
if _, ok := response["fallback"]; ok {
145+
t.Error("Cross-app replay should not contain 'fallback' field")
146+
}
147+
} else {
148+
if _, ok := response["timeout"]; !ok {
149+
t.Error("Response should contain 'timeout' field")
150+
}
151+
if _, ok := response["fallback"]; !ok {
152+
t.Error("Response should contain 'fallback' field")
153+
}
144154
}
145155

146156
} else {

internal/server/fly_replay_test.go

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -199,51 +199,39 @@ func TestHandleFlyReplay(t *testing.T) {
199199
}
200200
}
201201

202-
// Verify timeout and fallback fields are present
203-
if _, ok := response["timeout"]; !ok {
204-
t.Error("Response should contain 'timeout' field")
205-
}
206-
if _, ok := response["fallback"]; !ok {
207-
t.Error("Response should contain 'fallback' field")
208-
}
209-
if response["timeout"] != DefaultFlyReplayTimeout {
210-
t.Errorf("timeout = %v, expected %v", response["timeout"], DefaultFlyReplayTimeout)
211-
}
212-
if response["fallback"] != DefaultFlyReplayFallback {
213-
t.Errorf("fallback = %v, expected %v", response["fallback"], DefaultFlyReplayFallback)
214-
}
215-
216-
// Check for transform headers
217-
// Same-app and region replays should have transform with X-Navigator-Retry
218-
// Different-app replays should not have transform
219-
if strings.HasPrefix(tt.target, "machine=") {
202+
// Cross-app replays should NOT have timeout/fallback (let Fly handle natively)
203+
// Same-app (machine/region) replays SHOULD have timeout/fallback
204+
// Determine if this is a cross-app replay
205+
isCrossApp := false
206+
if strings.HasPrefix(tt.target, "app=") {
207+
isCrossApp = strings.TrimPrefix(tt.target, "app=") != tt.currentApp
208+
} else if strings.HasPrefix(tt.target, "machine=") {
220209
parts := strings.Split(strings.TrimPrefix(tt.target, "machine="), ":")
221210
if len(parts) == 2 {
222-
if parts[1] == tt.currentApp {
223-
if _, ok := response["transform"]; !ok {
224-
t.Error("Same-app replay should contain transform with retry header")
225-
}
226-
} else {
227-
if _, ok := response["transform"]; ok {
228-
t.Error("Different-app replay should not contain transform")
229-
}
230-
}
211+
isCrossApp = parts[1] != tt.currentApp
231212
}
232-
} else if strings.HasPrefix(tt.target, "app=") {
233-
appName := strings.TrimPrefix(tt.target, "app=")
234-
if appName == tt.currentApp {
235-
if _, ok := response["transform"]; !ok {
236-
t.Error("Same-app replay should contain transform with retry header")
237-
}
238-
} else {
239-
if _, ok := response["transform"]; ok {
240-
t.Error("Different-app replay should not contain transform")
241-
}
213+
}
214+
if isCrossApp {
215+
if _, ok := response["timeout"]; ok {
216+
t.Error("Cross-app replay should not contain 'timeout' field")
217+
}
218+
if _, ok := response["fallback"]; ok {
219+
t.Error("Cross-app replay should not contain 'fallback' field")
220+
}
221+
if _, ok := response["transform"]; ok {
222+
t.Error("Cross-app replay should not contain transform")
242223
}
243224
} else {
244-
// Region-based always has transform
225+
if response["timeout"] != DefaultFlyReplayTimeout {
226+
t.Errorf("timeout = %v, expected %v", response["timeout"], DefaultFlyReplayTimeout)
227+
}
228+
if response["fallback"] != DefaultFlyReplayFallback {
229+
t.Errorf("fallback = %v, expected %v", response["fallback"], DefaultFlyReplayFallback)
230+
}
231+
232+
// Same-app and region replays should have transform with X-Navigator-Retry
245233
if _, ok := response["transform"]; !ok {
246-
t.Error("Region-based replay should contain transform with retry header")
234+
t.Error("Same-app/region replay should contain transform with retry header")
247235
}
248236
}
249237
})

0 commit comments

Comments
 (0)