Refactor API client to use resolveApiRequestUrl for improved URL resolution, enhanced error handling, and clearer cross-origin request support. Add test script to package.json.
This commit is contained in:
+49
-78
@@ -1,68 +1,5 @@
|
||||
import { API_PATHS } from '../app/config.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();
|
||||
}
|
||||
import { resolveApiRequestUrl } from './url.js';
|
||||
|
||||
async function parseJsonResponse(response) {
|
||||
const text = await response.text();
|
||||
@@ -154,14 +91,31 @@ function extractErrorMessage(response, payload) {
|
||||
|
||||
function normalizeError(response, payload, request) {
|
||||
const details = flattenErrorObject(payload?.errors || payload?.details) || null;
|
||||
const message =
|
||||
extractErrorMessage(response, payload);
|
||||
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 error = new Error(message, {
|
||||
cause: {
|
||||
status: response.status,
|
||||
details,
|
||||
url: request.url,
|
||||
url: request.resolvedUrl,
|
||||
fetchUrl: request.fetchUrl,
|
||||
method: request.method,
|
||||
payload,
|
||||
},
|
||||
@@ -169,7 +123,8 @@ function normalizeError(response, payload, request) {
|
||||
|
||||
error.name = 'ApiRequestError';
|
||||
error.status = response.status;
|
||||
error.url = request.url;
|
||||
error.url = request.resolvedUrl;
|
||||
error.fetchUrl = request.fetchUrl;
|
||||
error.method = request.method;
|
||||
error.details = details;
|
||||
error.payload = payload;
|
||||
@@ -189,8 +144,10 @@ export async function apiRequest(store, path, options = {}) {
|
||||
}
|
||||
|
||||
const method = options.method || 'GET';
|
||||
const url = buildUrl({
|
||||
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
const requestTarget = resolveApiRequestUrl({
|
||||
baseUrl: config.baseUrl,
|
||||
currentOrigin,
|
||||
database: config.database,
|
||||
kitchenId: activeKitchen?.id,
|
||||
path,
|
||||
@@ -210,7 +167,7 @@ export async function apiRequest(store, path, options = {}) {
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
response = await fetch(requestTarget.fetchUrl, {
|
||||
method,
|
||||
headers,
|
||||
credentials: options.credentials || 'same-origin',
|
||||
@@ -220,21 +177,29 @@ 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(
|
||||
`Network request failed for ${method} ${url}. Check the server URL, nginx proxy, and CORS configuration.`,
|
||||
`${method} ${requestTarget.resolvedUrl} failed before a response was returned. ${networkHint}`,
|
||||
{
|
||||
cause: {
|
||||
url,
|
||||
url: requestTarget.resolvedUrl,
|
||||
fetchUrl: requestTarget.fetchUrl,
|
||||
method,
|
||||
isCrossOrigin: requestTarget.isCrossOrigin,
|
||||
originalError: error,
|
||||
},
|
||||
},
|
||||
);
|
||||
networkError.name = 'ApiNetworkError';
|
||||
networkError.url = url;
|
||||
networkError.url = requestTarget.resolvedUrl;
|
||||
networkError.fetchUrl = requestTarget.fetchUrl;
|
||||
networkError.method = method;
|
||||
networkError.isCrossOrigin = requestTarget.isCrossOrigin;
|
||||
logApiFailure('API network request failed', {
|
||||
url,
|
||||
url: requestTarget.resolvedUrl,
|
||||
fetchUrl: requestTarget.fetchUrl,
|
||||
method,
|
||||
error,
|
||||
});
|
||||
@@ -243,9 +208,14 @@ export async function apiRequest(store, path, options = {}) {
|
||||
|
||||
const payload = await parseResponse(response);
|
||||
if (!response.ok) {
|
||||
const apiError = normalizeError(response, payload, { url, method });
|
||||
const apiError = normalizeError(response, payload, {
|
||||
resolvedUrl: requestTarget.resolvedUrl,
|
||||
fetchUrl: requestTarget.fetchUrl,
|
||||
method,
|
||||
});
|
||||
logApiFailure('API request returned an error response', {
|
||||
url,
|
||||
url: requestTarget.resolvedUrl,
|
||||
fetchUrl: requestTarget.fetchUrl,
|
||||
method,
|
||||
status: response.status,
|
||||
payload,
|
||||
@@ -261,12 +231,13 @@ export function getPath(key) {
|
||||
}
|
||||
|
||||
export function buildKitchenApiUrl(store, path, query = {}) {
|
||||
return buildUrl({
|
||||
return resolveApiRequestUrl({
|
||||
baseUrl: store.config.baseUrl,
|
||||
currentOrigin: typeof window !== 'undefined' ? window.location.origin : '',
|
||||
database: store.config.database,
|
||||
kitchenId: store.activeKitchen?.id,
|
||||
path,
|
||||
query,
|
||||
includeKitchen: true,
|
||||
});
|
||||
}).resolvedUrl;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user