blob: dc1eed3594d6fbff8bab5a755f0c160285394e19 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { withRuntimeBasePath } from '../runtimeBasePath';
import { getCsrfToken, setCsrfToken } from './csrf';
import type { UiErrorBody } from './types';
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
export class UiApiError extends Error {
readonly status: number;
readonly code: string;
readonly requestId: string;
readonly details?: unknown;
constructor(status: number, body: UiErrorBody) {
super(body.message);
this.name = 'UiApiError';
this.status = status;
this.code = body.code;
this.requestId = body.requestId ?? 'unknown';
this.details = body.details;
}
}
function isUiErrorBody(value: unknown): value is UiErrorBody {
if (!value || typeof value !== 'object') return false;
const candidate = value as Partial<UiErrorBody>;
return (
typeof candidate.code === 'string' &&
typeof candidate.message === 'string'
);
}
function fallbackError(status: number, requestId: string): UiErrorBody {
const errors: Record<number, Pick<UiErrorBody, 'code' | 'message'>> = {
400: { code: 'UI_BAD_REQUEST', message: 'The request is invalid.' },
401: { code: 'UI_UNAUTHENTICATED', message: 'Authentication is required.' },
403: { code: 'UI_FORBIDDEN', message: 'You do not have permission to perform this operation.' },
404: { code: 'UI_NOT_FOUND', message: 'The requested resource was not found.' },
409: { code: 'UI_CONFLICT', message: 'The request conflicts with the current state.' },
413: { code: 'UI_PAYLOAD_TOO_LARGE', message: 'The request is too large.' },
429: { code: 'UI_RATE_LIMITED', message: 'Too many requests. Please try again later.' },
};
const known = errors[status];
if (known) return { ...known, requestId };
if (status >= 500) {
return { code: 'UI_SERVER_ERROR', message: 'The server could not complete the request.', requestId };
}
return {
code: 'UI_REQUEST_FAILED',
message: 'The request could not be completed.',
requestId,
};
}
export async function uiRequest<T>(path: string, init: RequestInit = {}): Promise<T> {
if (!path.startsWith('/rest/v1/ui/') && !path.startsWith('/rest/v1/sql-sessions')) {
throw new Error('uiRequest only accepts UI bootstrap and Web SQL paths.');
}
const method = (init.method ?? 'GET').toUpperCase();
const headers = new Headers(init.headers);
headers.set('Accept', 'application/json');
if (init.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
if (MUTATING_METHODS.has(method)) {
const token = getCsrfToken();
if (token) headers.set('X-Doris-CSRF-Token', token);
}
const response = await fetch(withRuntimeBasePath(path), {
...init,
method,
headers,
credentials: 'same-origin',
});
const requestId = response.headers.get('X-Request-Id') ?? 'unknown';
const contentType = response.headers.get('Content-Type') ?? '';
let payload: unknown = null;
if (contentType.includes('application/json')) {
try {
payload = await response.json();
} catch {
throw new UiApiError(response.status, {
code: 'UI_INVALID_RESPONSE',
message: 'The server returned invalid JSON.',
requestId,
});
}
}
if (!response.ok) {
const error = isUiErrorBody(payload) ? payload : fallbackError(response.status, requestId);
if (response.status === 401) {
setCsrfToken(null);
window.dispatchEvent(new CustomEvent('doris-ui:unauthorized'));
}
throw new UiApiError(response.status, error);
}
if (!payload || typeof payload !== 'object') {
throw new UiApiError(response.status, {
code: 'UI_INVALID_RESPONSE',
message: 'The server returned an invalid response.',
requestId,
});
}
// Accept the former {data, requestId} shape while an already-running FE is
// being replaced, but new UI endpoints return their business object directly.
if ('data' in payload && 'requestId' in payload) {
return (payload as { data: T }).data;
}
return payload as T;
}