Eugeny/tabby · error · Error

${method} ${url} failed: ${response.status} ${response.statu

Error message

${method} ${url} failed: ${response.status} ${response.statusText}

What it means

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.

Source

Thrown at tabby-settings/src/services/configSync.service.ts:188

        // arbitrary command execution on the next sync. Require HTTPS.
        if (!/^https:\/\//i.test(host)) {
            const message = `Config sync host must use HTTPS (got: ${host})`
            this.logger.error(message)
            throw new Error(message)
        }
        url = host + url
        this.logger.debug(`${method} ${url}`, data)
        try {
            const response = await fetch(url, {
                method,
                headers: {
                    Authorization: `Bearer ${this.config.store.configSync.token}`,
                    ...data !== undefined ? { 'Content-Type': 'application/json' } : {},
                },
                body: data !== undefined ? JSON.stringify(data) : undefined,
            })
            if (!response.ok) {
                throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}`)
            }
            this.logger.debug(response)
            // ponytail: DELETE returns empty 204, parse only if there's a body
            const text = await response.text()
            return text ? JSON.parse(text) : undefined
        } catch (error) {
            this.logger.error(error)
            throw error
        }
    }

    private async autoSync () {
        while (true) {
            try {
                if (this.isEnabled() && this.config.store.configSync.auto) {
                    const cfg = await this.getConfig(this.config.store.configSync.configID)
                    if (new Date(cfg.modified_at) > this.lastRemoteChange) {
                        this.logger.info('Remote config changed, downloading')

View on GitHub (pinned to 14e2d60b9b)

Solutions

  1. 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.
  2. Refresh the access token and retry the sync operation.
  3. Confirm the sync server endpoint matches what this Tabby version expects (API version drift).
  4. Wrap sync calls in try/catch, surface the status to the user, and offer a retry with exponential backoff for transient 5xx/429.

Example fix

// before
if (!response.ok) throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}`)

// after - structured error + retry for transient failures
if (!response.ok) {
    const err = new Error(`${method} ${url} failed: ${response.status} ${response.statusText}`)
    ;(err as any).status = response.status
    ;(err as any).transient = response.status >= 500 || response.status === 429
    throw err
}
Defensive patterns

Strategy: retry

Validate before calling

function isTransientStatus (status: number): boolean {
    return status >= 500 || status === 429
}

// pre-flight: refresh token if known to be near expiry
if (tokenExpired(config.store.configSync.token)) {
    await refreshToken()
}

Type guard

function isHttpError (e: unknown): e is Error & { status?: number } {
    return e instanceof Error && typeof (e as any).status === 'number'
}

Try / catch

async function syncWithRetry (fn: () => Promise<any>, attempts = 3) {
    for (let i = 0; i < attempts; i++) {
        try { return await fn() }
        catch (e) {
            const status = (e as any)?.status ?? parseInt(/failed: (\d+)/.exec(String(e))?.[1] ?? '0', 10)
            if (!isTransientStatus(status) || i === attempts - 1) throw e
            await new Promise(r => setTimeout(r, 2 ** i * 500))
        }
    }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of Eugeny/tabby@14e2d60b9b (2026-08-12). Data as JSON: /api/errors/613d10e972531434. Report an issue: GitHub.