Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 69 additions & 35 deletions web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,35 +100,84 @@ class ApiClient {
})

if (!response.ok) {
// Don't auto-redirect for auth check endpoints - let React Router handle navigation
const isAuthCheckEndpoint = endpoint === "/auth/me" || endpoint === "/auth/validate"
const errorMessage = await this.extractErrorMessage(response)
this.handleAuthError(response.status, endpoint, errorMessage)
throw new Error(errorMessage)
}

// Handle empty responses (like 204 No Content)
if (response.status === 204 || response.headers.get("content-length") === "0") {
return undefined as T
}

if ((response.status === 401 || response.status === 403) && !isAuthCheckEndpoint && !window.location.pathname.startsWith(withBasePath("/login")) && !window.location.pathname.startsWith(withBasePath("/setup"))) {
window.location.href = withBasePath("/login")
throw new Error("Session expired")
return response.json()
}

private async extractErrorMessage(response: Response): Promise<string> {
const fallbackMessage = `HTTP error! status: ${response.status}`

try {
const rawBody = await response.text()
if (!rawBody) {
return fallbackMessage
}

let errorMessage = `HTTP error! status: ${response.status}`
try {
const errorData = await response.json()
errorMessage = errorData.error || errorData.message || errorMessage
const errorData = JSON.parse(rawBody) as { error?: string; message?: string }
const parsedMessage = errorData?.error ?? errorData?.message
if (typeof parsedMessage === "string" && parsedMessage.trim().length > 0) {
return parsedMessage
}
} catch {
try {
const errorText = await response.text()
errorMessage = errorText || errorMessage
} catch {
// nothing to see here
const trimmed = rawBody.trim()
if (trimmed.length > 0) {
return trimmed
}
}
throw new Error(errorMessage)

return fallbackMessage
} catch {
return fallbackMessage
}
}

// Handle empty responses (like 204 No Content)
if (response.status === 204 || response.headers.get("content-length") === "0") {
return undefined as T
private handleAuthError(status: number, endpoint: string, errorMessage: string): void {
if (!this.shouldForceLogout(status, endpoint, errorMessage)) {
return
}

return response.json()
window.location.href = withBasePath("/login")
throw new Error("Session expired")
}

private shouldForceLogout(status: number, endpoint: string, errorMessage: string): boolean {
if (typeof window === "undefined") {
return false
}

if (this.isAuthCheckEndpoint(endpoint)) {
return false
}

const pathname = window.location.pathname
if (pathname.startsWith(withBasePath("/login")) || pathname.startsWith(withBasePath("/setup"))) {
return false
}

if (status === 401) {
return true
}

if (status === 403) {
const normalizedMessage = errorMessage.trim().toLowerCase()
return normalizedMessage === "unauthorized"
}

return false
}

private isAuthCheckEndpoint(endpoint: string): boolean {
return endpoint === "/auth/me" || endpoint === "/auth/validate"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Auth endpoints
Expand Down Expand Up @@ -993,23 +1042,8 @@ class ApiClient {
})

if (!response.ok) {
if ((response.status === 401 || response.status === 403) && !window.location.pathname.startsWith(withBasePath("/login")) && !window.location.pathname.startsWith(withBasePath("/setup"))) {
window.location.href = withBasePath("/login")
throw new Error("Session expired")
}

let errorMessage = `HTTP error! status: ${response.status}`
try {
const errorData = await response.json()
errorMessage = errorData.error || errorData.message || errorMessage
} catch {
try {
const errorText = await response.text()
errorMessage = errorText || errorMessage
} catch {
// nothing to see here
}
}
const errorMessage = await this.extractErrorMessage(response)
this.handleAuthError(response.status, `/instances/${instanceId}/torrents/${encodedHash}/export`, errorMessage)
throw new Error(errorMessage)
}

Expand Down
Loading