Refactor API client to replace resolveApiRequestUrl with buildUrl, improving URL construction and error handling. Remove test script from package.json.

This commit is contained in:
2026-04-06 11:06:07 +02:00
parent 739cb37157
commit d56da613dd
2 changed files with 79 additions and 51 deletions
+1 -2
View File
@@ -6,8 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "node --test"
"preview": "vite preview"
},
"dependencies": {
"alpinejs": "^3.14.9",
+78 -49
View File
@@ -1,5 +1,68 @@
import { API_PATHS } from '../app/config.js';
import { resolveApiRequestUrl } from './url.js';
function normalizeBaseUrl(baseUrl) {
return String(baseUrl || '').trim().replace(/\/+$/, '');
}
function isSameOriginBaseUrl(baseUrl) {
if (!baseUrl) {
return true;
}
if (typeof window === 'undefined') {
return false;
}
try {
return new URL(baseUrl, window.location.origin).origin === window.location.origin;
} catch {
return false;
}
}
function buildPathname({ database, kitchenId, path, includeKitchen = true }) {
const encodedDatabase = encodeURIComponent(database);
const encodedPathSegments = String(path || '')
.replace(/^\/+/, '')
.split('/')
.filter(Boolean)
.map((segment) => encodeURIComponent(segment));
const segments = [encodedDatabase];
if (includeKitchen && kitchenId) {
segments.push('kitchen', encodeURIComponent(String(kitchenId)));
}
segments.push(...encodedPathSegments);
return `/${segments.join('/')}`;
}
function buildUrl({ baseUrl, database, kitchenId, path, query = {}, includeKitchen = true }) {
const cleanBaseUrl = normalizeBaseUrl(baseUrl);
const pathname = buildPathname({
database,
kitchenId,
path,
includeKitchen,
});
const searchParams = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
searchParams.set(key, String(value));
}
});
const queryString = searchParams.toString();
const relativeUrl = queryString ? `${pathname}?${queryString}` : pathname;
if (!cleanBaseUrl || isSameOriginBaseUrl(cleanBaseUrl)) {
return relativeUrl;
}
return new URL(relativeUrl, `${cleanBaseUrl}/`).toString();
}
async function parseJsonResponse(response) {
const text = await response.text();
@@ -91,31 +154,14 @@ function extractErrorMessage(response, payload) {
function normalizeError(response, payload, request) {
const details = flattenErrorObject(payload?.errors || payload?.details) || null;
const payloadMessage = extractErrorMessage(response, payload);
const statusHint =
response.status === 404
? 'This usually means the request hit the wrong origin, port, or path.'
: response.status === 401 || response.status === 403
? 'Authentication or application-key validation failed.'
: response.status >= 500
? 'The backend reported a server error.'
: '';
const message = [
`${request.method} ${request.resolvedUrl} returned ${response.status}.`,
statusHint,
payloadMessage && !payloadMessage.startsWith('Request failed with status')
? payloadMessage
: '',
]
.filter(Boolean)
.join(' ');
const message =
extractErrorMessage(response, payload);
const error = new Error(message, {
cause: {
status: response.status,
details,
url: request.resolvedUrl,
fetchUrl: request.fetchUrl,
url: request.url,
method: request.method,
payload,
},
@@ -123,8 +169,7 @@ function normalizeError(response, payload, request) {
error.name = 'ApiRequestError';
error.status = response.status;
error.url = request.resolvedUrl;
error.fetchUrl = request.fetchUrl;
error.url = request.url;
error.method = request.method;
error.details = details;
error.payload = payload;
@@ -144,10 +189,8 @@ export async function apiRequest(store, path, options = {}) {
}
const method = options.method || 'GET';
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
const requestTarget = resolveApiRequestUrl({
const url = buildUrl({
baseUrl: config.baseUrl,
currentOrigin,
database: config.database,
kitchenId: activeKitchen?.id,
path,
@@ -167,7 +210,7 @@ export async function apiRequest(store, path, options = {}) {
let response;
try {
response = await fetch(requestTarget.fetchUrl, {
response = await fetch(url, {
method,
headers,
credentials: options.credentials || 'same-origin',
@@ -177,29 +220,21 @@ export async function apiRequest(store, path, options = {}) {
: options.body || undefined,
});
} catch (error) {
const networkHint = requestTarget.isCrossOrigin
? 'Cross-origin request failed before a usable response was returned. Check nginx CORS/preflight handling and that the configured API origin is reachable.'
: 'Request failed before a response was returned. Check that the current origin serves the API or configure the Tryton server URL explicitly.';
const networkError = new Error(
`${method} ${requestTarget.resolvedUrl} failed before a response was returned. ${networkHint}`,
`Network request failed for ${method} ${url}. Check the server URL, nginx proxy, and CORS configuration.`,
{
cause: {
url: requestTarget.resolvedUrl,
fetchUrl: requestTarget.fetchUrl,
url,
method,
isCrossOrigin: requestTarget.isCrossOrigin,
originalError: error,
},
},
);
networkError.name = 'ApiNetworkError';
networkError.url = requestTarget.resolvedUrl;
networkError.fetchUrl = requestTarget.fetchUrl;
networkError.url = url;
networkError.method = method;
networkError.isCrossOrigin = requestTarget.isCrossOrigin;
logApiFailure('API network request failed', {
url: requestTarget.resolvedUrl,
fetchUrl: requestTarget.fetchUrl,
url,
method,
error,
});
@@ -208,14 +243,9 @@ export async function apiRequest(store, path, options = {}) {
const payload = await parseResponse(response);
if (!response.ok) {
const apiError = normalizeError(response, payload, {
resolvedUrl: requestTarget.resolvedUrl,
fetchUrl: requestTarget.fetchUrl,
method,
});
const apiError = normalizeError(response, payload, { url, method });
logApiFailure('API request returned an error response', {
url: requestTarget.resolvedUrl,
fetchUrl: requestTarget.fetchUrl,
url,
method,
status: response.status,
payload,
@@ -231,13 +261,12 @@ export function getPath(key) {
}
export function buildKitchenApiUrl(store, path, query = {}) {
return resolveApiRequestUrl({
return buildUrl({
baseUrl: store.config.baseUrl,
currentOrigin: typeof window !== 'undefined' ? window.location.origin : '',
database: store.config.database,
kitchenId: store.activeKitchen?.id,
path,
query,
includeKitchen: true,
}).resolvedUrl;
});
}