mirror of
https://github.com/immich-app/immich
synced 2026-08-29 13:15:45 +00:00
fix: throw a typed error on malformed api responses (#31006)
* fix: throw a typed error on malformed api responses * fix: drop response body parse check * fix(web): keep json response validation when overriding fetch (#31008) * fix(cli): report malformed api responses (#31007)
This commit is contained in:
parent
4ff7148f85
commit
6221e29c70
4 changed files with 70 additions and 4 deletions
|
|
@ -1,4 +1,12 @@
|
|||
import { ApiKeyResponseDto, getMyApiKey, getMyUser, init, isHttpError, Permission } from '@immich/sdk';
|
||||
import {
|
||||
ApiKeyResponseDto,
|
||||
getMyApiKey,
|
||||
getMyUser,
|
||||
init,
|
||||
isHttpError,
|
||||
isMalformedResponseError,
|
||||
Permission,
|
||||
} from '@immich/sdk';
|
||||
import { convertPathToPattern, glob } from 'fast-glob';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
|
|
@ -82,7 +90,7 @@ export const connect = async (url: string, key: string) => {
|
|||
init({ baseUrl: url, apiKey: key });
|
||||
|
||||
const [error] = await withError(getMyUser());
|
||||
if (isHttpError(error)) {
|
||||
if (isHttpError(error) || isMalformedResponseError(error)) {
|
||||
logError(error, `Failed to connect to server ${url}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -94,6 +102,11 @@ export const logError = (error: unknown, message: string) => {
|
|||
if (isHttpError(error)) {
|
||||
console.error(`${message}: ${error.status}`);
|
||||
console.error(JSON.stringify(error.data, undefined, 2));
|
||||
} else if (isMalformedResponseError(error)) {
|
||||
console.error(`${message}: ${error.message}`);
|
||||
console.error(
|
||||
'Check that the URL points at the Immich API, and that nothing in front of it (reverse proxy, SSO portal) is answering instead.',
|
||||
);
|
||||
} else {
|
||||
console.error(`${message} - ${error}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,3 +20,24 @@ export interface ApiHttpError extends HttpError {
|
|||
export function isHttpError(error: unknown): error is ApiHttpError {
|
||||
return error instanceof HttpError;
|
||||
}
|
||||
|
||||
export class MalformedResponseError extends Error {
|
||||
override name = 'MalformedResponseError';
|
||||
|
||||
constructor(
|
||||
reason: string,
|
||||
readonly url: string,
|
||||
readonly status: number,
|
||||
readonly contentType: string | null,
|
||||
) {
|
||||
super(
|
||||
`${reason} (${url}, HTTP ${status}, content-type: ${contentType ?? 'none'})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isMalformedResponseError(
|
||||
error: unknown,
|
||||
): error is MalformedResponseError {
|
||||
return error instanceof MalformedResponseError;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { defaults } from './fetch-client.js';
|
||||
import { MalformedResponseError } from './fetch-errors.js';
|
||||
|
||||
export * from './fetch-client.js';
|
||||
export * from './fetch-errors.js';
|
||||
|
|
@ -48,6 +49,37 @@ const assertNoApiKey = (headerKey: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const jsonOnly =
|
||||
(impl?: typeof fetch): typeof fetch =>
|
||||
async (input, options) => {
|
||||
const response = await (impl ?? fetch)(input, options);
|
||||
|
||||
const expectsJson = new Headers(options?.headers)
|
||||
.get('accept')
|
||||
?.includes('json');
|
||||
if (!expectsJson || response.status === 204) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType?.includes('json')) {
|
||||
throw new MalformedResponseError(
|
||||
'Expected a JSON response',
|
||||
response.url,
|
||||
response.status,
|
||||
contentType,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const setFetch = (impl: typeof fetch) => {
|
||||
defaults.fetch = jsonOnly(impl);
|
||||
};
|
||||
|
||||
defaults.fetch = jsonOnly();
|
||||
|
||||
export const getAssetOriginalPath = (id: string) => `/assets/${id}/original`;
|
||||
|
||||
export const getAssetThumbnailPath = (id: string) => `/assets/${id}/thumbnail`;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaults } from '@immich/sdk';
|
||||
import { setFetch } from '@immich/sdk';
|
||||
import { memoize } from 'lodash-es';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
|
|
@ -11,7 +11,7 @@ async function _init(fetch: Fetch) {
|
|||
// set event.fetch on the fetch-client used by @immich/sdk
|
||||
// https://kit.svelte.dev/docs/load#making-fetch-requests
|
||||
// https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options
|
||||
defaults.fetch = fetch;
|
||||
setFetch(fetch);
|
||||
await initLanguage();
|
||||
await serverConfigManager.init();
|
||||
await authManager.load();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue