Skip to content

Commit de41460

Browse files
committed
fix(review): bound retry session.create, compose BIND fallback, entrypoint comment
- Wrap all 4 retry-path session.create (chat stream/non-stream, responses + messages non-stream) in withTimeout so a hung backend 504s instead of stalling past REQUEST_TIMEOUT_MS; regression test - Compose BIND_HOST chains OPENCODE_PROXY_BIND_HOST fallback instead of shadowing it with a hard default - Entrypoint wait comment distinguishes fast-refused (~180s, covered by start_period 200s) from hung-backend worst case (~630s, wedged)
1 parent 453a228 commit de41460

4 files changed

Lines changed: 39 additions & 5 deletions

File tree

‎docker-compose.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ services:
1313
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-}
1414
- OPENCODE_ZEN_API_KEY=${OPENCODE_ZEN_API_KEY:-}
1515
- API_KEY=${API_KEY:-}
16-
- BIND_HOST=${BIND_HOST:-0.0.0.0}
16+
- BIND_HOST=${BIND_HOST:-${OPENCODE_PROXY_BIND_HOST:-0.0.0.0}}
1717
- DISABLE_TOOLS=${DISABLE_TOOLS:-true}
1818
- OPENCODE_DISABLE_TOOLS=${OPENCODE_DISABLE_TOOLS:-}
1919
- OPENCODE_USE_ISOLATED_HOME=${OPENCODE_USE_ISOLATED_HOME:-false}

‎entrypoint.sh‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ if [[ "$1" == "opencode" && "$2" == "serve" ]]; then
4646

4747
echo "Waiting for OpenCode Server to become available..."
4848
# Cold start on small (1GB) hosts can take minutes; do not kill early.
49+
# Budget: 90 x 2s sleep ~= 180s when the backend refuses fast (normal
50+
# cold start; covered by compose start_period 200s). If the backend
51+
# accepts but hangs every probe, each round costs up to curl -m 5 + 2s
52+
# sleep (worst case ~630s) — that means the backend itself is wedged,
53+
# not slow, and the container will exit 1 so it can be rescheduled.
4954
MAX_RETRIES=90
5055
RETRY_DELAY=2
5156
COUNT=0

‎src/proxy.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,7 +1961,8 @@ export function createApp(config) {
19611961
} catch (e) {
19621962
logDebug('Failed to delete retried session', { sessionId, error: e.message });
19631963
}
1964-
const retrySessionRes = await client.session.create();
1964+
const retrySessionRes = await withTimeout(
1965+
client.session.create(), REQUEST_TIMEOUT_MS, 'create session');
19651966
sessionId = retrySessionRes.data?.id;
19661967
if (!sessionId) throw new Error('Failed to create OpenCode session for retry');
19671968
promptParams.path.id = sessionId;
@@ -2174,7 +2175,8 @@ export function createApp(config) {
21742175
} catch (e) {
21752176
logDebug('Failed to delete retried session', { sessionId, error: e.message });
21762177
}
2177-
const retrySessionRes = await client.session.create();
2178+
const retrySessionRes = await withTimeout(
2179+
client.session.create(), REQUEST_TIMEOUT_MS, 'create session');
21782180
sessionId = retrySessionRes.data?.id;
21792181
if (!sessionId) throw new Error('Failed to create OpenCode session for retry');
21802182
promptParams.path.id = sessionId;
@@ -3073,7 +3075,8 @@ export function createApp(config) {
30733075
try { await client.session.delete({ path: { id: sessionId } }); } catch (e) {
30743076
logDebug('Failed to delete retried session', { sessionId, error: e.message });
30753077
}
3076-
const retrySessionRes = await client.session.create();
3078+
const retrySessionRes = await withTimeout(
3079+
client.session.create(), REQUEST_TIMEOUT_MS, 'create session');
30773080
sessionId = retrySessionRes.data?.id;
30783081
if (!sessionId) throw new Error('Failed to create OpenCode session for retry');
30793082
promptParams.path.id = sessionId;
@@ -3397,7 +3400,8 @@ export function createApp(config) {
33973400
}
33983401
if (attempt > 1) {
33993402
try { await client.session.delete({ path: { id: sessionId } }); } catch {}
3400-
const r = await client.session.create();
3403+
const r = await withTimeout(
3404+
client.session.create(), REQUEST_TIMEOUT_MS, 'create session');
34013405
sessionId = r.data?.id;
34023406
if (!sessionId) throw new Error('Failed to create OpenCode session for retry');
34033407
promptParams.path.id = sessionId;

‎tests/stream-hardening.test.js‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,31 @@ describe('POST /v1/responses stream hardening', () => {
125125
expect(res.statusCode).toBe(504);
126126
expect(JSON.stringify(res.body)).not.toContain('"Object"');
127127
}, 20000);
128+
129+
test('hung retry session.create surfaces a 504 instead of stalling', async () => {
130+
// Attempt 1 fails transiently (fast backoff via retry-after-ms),
131+
// then the retry's session.create hangs: must 504, not stall.
132+
sdkMocks.sessionCreate
133+
.mockImplementationOnce(async () => ({ data: { id: SESSION_ID } }))
134+
.mockImplementationOnce(() => new Promise(() => {}));
135+
sdkMocks.sessionPrompt.mockImplementationOnce(async () => {
136+
throw {
137+
name: 'CreditsError',
138+
data: {
139+
message: '429: {"message":"Too many requests","type":"CreditsError"}',
140+
responseHeaders: { 'retry-after-ms': '10' }
141+
}
142+
};
143+
});
144+
const started = Date.now();
145+
const res = await request(buildApp({ REQUEST_TIMEOUT_MS: 400, RETRY_MAX_RETRIES: 1 }))
146+
.post('/v1/responses')
147+
.set('Authorization', 'Bearer test-key')
148+
.send({ model: 'big-pickle', input: 'hi' });
149+
expect(Date.now() - started).toBeLessThan(15000);
150+
expect(res.statusCode).toBe(504);
151+
expect(JSON.stringify(res.body)).not.toContain('"Object"');
152+
}, 20000);
128153
});
129154

130155
describe('POST /v1/messages stream does not cancel itself', () => {

0 commit comments

Comments
 (0)