{"record":{"id":"613d10e972531434","repo":"Eugeny/tabby","slug":"method-url-failed-response-status-resp","errorCode":null,"errorMessage":"${method} ${url} failed: ${response.status} ${response.statusText}","messagePattern":"(.+?) (.+?) failed: (.+?) (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tabby-settings/src/services/configSync.service.ts","lineNumber":188,"sourceCode":"        // arbitrary command execution on the next sync. Require HTTPS.\n        if (!/^https:\\/\\//i.test(host)) {\n            const message = `Config sync host must use HTTPS (got: ${host})`\n            this.logger.error(message)\n            throw new Error(message)\n        }\n        url = host + url\n        this.logger.debug(`${method} ${url}`, data)\n        try {\n            const response = await fetch(url, {\n                method,\n                headers: {\n                    Authorization: `Bearer ${this.config.store.configSync.token}`,\n                    ...data !== undefined ? { 'Content-Type': 'application/json' } : {},\n                },\n                body: data !== undefined ? JSON.stringify(data) : undefined,\n            })\n            if (!response.ok) {\n                throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}`)\n            }\n            this.logger.debug(response)\n            // ponytail: DELETE returns empty 204, parse only if there's a body\n            const text = await response.text()\n            return text ? JSON.parse(text) : undefined\n        } catch (error) {\n            this.logger.error(error)\n            throw error\n        }\n    }\n\n    private async autoSync () {\n        while (true) {\n            try {\n                if (this.isEnabled() && this.config.store.configSync.auto) {\n                    const cfg = await this.getConfig(this.config.store.configSync.configID)\n                    if (new Date(cfg.modified_at) > this.lastRemoteChange) {\n                        this.logger.info('Remote config changed, downloading')","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/Eugeny/tabby/blob/14e2d60b9b6dee84a53c37f05eefeb803787de04/tabby-settings/src/services/configSync.service.ts#L170-L206","documentation":"Thrown by `ConfigSyncService`'s fetch helper when the remote responds with a non-2xx status (`response.ok === false`). The message includes the HTTP method, full URL, status code, and status text to aid diagnosis. It is re-thrown after being logged so callers can handle transport/server failures.","triggerScenarios":"Any sync HTTP call (GET/POST/PUT/DELETE against the configured host) returning 4xx or 5xx: 401 (bad/expired token), 403 (forbidden), 404 (wrong endpoint/host), 409 (conflict), 500 (server error), 502/503 (gateway).","commonSituations":"Expired or wrong `configSync.token` (401); host reachable but path wrong (404); server-side bug during merge (500); rate limiting (429); network proxy rewriting responses.","solutions":["Inspect the status code in the message: 401/403 -> fix the bearer token in `configSync.token`; 404 -> verify host/path; 5xx -> server-side issue, retry with backoff.","Refresh the access token and retry the sync operation.","Confirm the sync server endpoint matches what this Tabby version expects (API version drift).","Wrap sync calls in try/catch, surface the status to the user, and offer a retry with exponential backoff for transient 5xx/429."],"exampleFix":"// before\nif (!response.ok) throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}`)\n\n// after - structured error + retry for transient failures\nif (!response.ok) {\n    const err = new Error(`${method} ${url} failed: ${response.status} ${response.statusText}`)\n    ;(err as any).status = response.status\n    ;(err as any).transient = response.status >= 500 || response.status === 429\n    throw err\n}","handlingStrategy":"retry","validationCode":"function isTransientStatus (status: number): boolean {\n    return status >= 500 || status === 429\n}\n\n// pre-flight: refresh token if known to be near expiry\nif (tokenExpired(config.store.configSync.token)) {\n    await refreshToken()\n}","typeGuard":"function isHttpError (e: unknown): e is Error & { status?: number } {\n    return e instanceof Error && typeof (e as any).status === 'number'\n}","tryCatchPattern":"async function syncWithRetry (fn: () => Promise<any>, attempts = 3) {\n    for (let i = 0; i < attempts; i++) {\n        try { return await fn() }\n        catch (e) {\n            const status = (e as any)?.status ?? parseInt(/failed: (\\d+)/.exec(String(e))?.[1] ?? '0', 10)\n            if (!isTransientStatus(status) || i === attempts - 1) throw e\n            await new Promise(r => setTimeout(r, 2 ** i * 500))\n        }\n    }\n}","preventionTips":["Keep the configSync token fresh; refresh before expiry.","Confirm the sync server endpoint version matches this Tabby build.","Surface the status code to the user so 401 vs 404 vs 500 is actionable.","Retry only transient (5xx/429) failures with exponential backoff; do not retry 4xx."],"tags":["config-sync","http","network","authentication","error-handling"],"backgroundTag":null,"analyzedSha":"14e2d60b9b6dee84a53c37f05eefeb803787de04","analyzedAt":"2026-08-12T11:46:48.773Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}