mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Backend Fetchers: improve
This commit is contained in:
@@ -4,7 +4,7 @@ import type { BackendCapabilities } from '~/modules/backend/store-backend-capabi
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { analyticsListCapabilities } from './backend.analytics';
|
||||
|
||||
@@ -72,9 +72,12 @@ export const backendRouter = createTRPCRouter({
|
||||
.input(z.object({ code: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
// Documented here: https://openrouter.ai/docs#oauth
|
||||
return await fetchJsonOrTRPCError<{ key: string }, { code: string }>('https://openrouter.ai/api/v1/auth/keys', 'POST', {}, {
|
||||
code: input.code,
|
||||
}, 'Backend.exchangeOpenRouterKey');
|
||||
return await fetchJsonOrTRPCThrow<{ key: string }, { code: string }>({
|
||||
url: 'https://openrouter.ai/api/v1/auth/keys',
|
||||
method: 'POST',
|
||||
body: { code: input.code },
|
||||
name: 'Backend.exchangeOpenRouterKey',
|
||||
});
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
|
||||
export const speechInputSchema = z.object({
|
||||
@@ -49,7 +49,11 @@ export const elevenlabsRouter = createTRPCRouter({
|
||||
const { elevenKey } = input;
|
||||
const { headers, url } = elevenlabsAccess(elevenKey, '/v1/voices');
|
||||
|
||||
const voicesList = await fetchJsonOrTRPCError<ElevenlabsWire.VoicesList>(url, 'GET', headers, undefined, 'ElevenLabs');
|
||||
const voicesList = await fetchJsonOrTRPCThrow<ElevenlabsWire.VoicesList>({
|
||||
url,
|
||||
headers,
|
||||
name: 'ElevenLabs',
|
||||
});
|
||||
|
||||
// bring category != 'premade' to the top
|
||||
voicesList.voices.sort((a, b) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { Search } from './search.types';
|
||||
|
||||
@@ -35,7 +35,11 @@ export const googleSearchRouter = createTRPCRouter({
|
||||
throw new Error('Missing API Key or Custom Search Engine ID');
|
||||
|
||||
const url = `https://www.googleapis.com/customsearch/v1?${objectToQueryString(customSearchParams)}`;
|
||||
const data: Search.Wire.SearchResponse & { error?: { message?: string } } = await fetchJsonOrTRPCError(url, 'GET', {}, undefined, 'Google Custom Search');
|
||||
const data: Search.Wire.SearchResponse & { error?: { message?: string } } = await fetchJsonOrTRPCThrow({
|
||||
url,
|
||||
name: 'Google Custom Search',
|
||||
});
|
||||
|
||||
if (data.error)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { fixupHost } from '~/common/util/urlUtils';
|
||||
|
||||
@@ -28,7 +28,7 @@ const DEFAULT_HELICONE_ANTHROPIC_HOST = 'anthropic.hconeai.com';
|
||||
|
||||
async function anthropicPOST<TOut extends object, TPostBody extends object>(access: AnthropicAccessSchema, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = anthropicAccess(access, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut, TPostBody>(url, 'POST', headers, body, 'Anthropic');
|
||||
return await fetchJsonOrTRPCThrow<TOut, TPostBody>({ url, method: 'POST', headers, body, name: 'Anthropic' });
|
||||
}
|
||||
|
||||
export function anthropicAccess(access: AnthropicAccessSchema, apiPath: string): { headers: HeadersInit, url: string } {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { env } from '~/server/env.mjs';
|
||||
import packageJson from '../../../../../package.json';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { fixupHost } from '~/common/util/urlUtils';
|
||||
import { llmsChatGenerateOutputSchema, llmsGenerateContextSchema, llmsListModelsOutputSchema } from '../llm.server.types';
|
||||
@@ -95,12 +95,12 @@ export const geminiGenerateContentTextPayload = (model: OpenAIModelSchema, histo
|
||||
|
||||
async function geminiGET<TOut extends object>(access: GeminiAccessSchema, modelRefId: string | null, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = geminiAccess(access, modelRefId, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut>(url, 'GET', headers, undefined, 'Gemini');
|
||||
return await fetchJsonOrTRPCThrow<TOut>({ url, headers, name: 'Gemini' });
|
||||
}
|
||||
|
||||
async function geminiPOST<TOut extends object, TPostBody extends object>(access: GeminiAccessSchema, modelRefId: string | null, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = geminiAccess(access, modelRefId, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut, TPostBody>(url, 'POST', headers, body, 'Gemini');
|
||||
return await fetchJsonOrTRPCThrow<TOut, TPostBody>({ url, method: 'POST', headers, body, name: 'Gemini' });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError, fetchTextOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow, fetchTextOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { LLM_IF_OAI_Chat } from '../../store-llms';
|
||||
|
||||
@@ -88,12 +88,12 @@ export function ollamaCompletionPayload(model: OpenAIModelSchema, history: OpenA
|
||||
|
||||
async function ollamaGET<TOut extends object>(access: OllamaAccessSchema, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = ollamaAccess(access, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut>(url, 'GET', headers, undefined, 'Ollama');
|
||||
return await fetchJsonOrTRPCThrow<TOut>({ url, headers, name: 'Ollama' });
|
||||
}
|
||||
|
||||
async function ollamaPOST<TOut extends object, TPostBody extends object>(access: OllamaAccessSchema, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = ollamaAccess(access, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut, TPostBody>(url, 'POST', headers, body, 'Ollama');
|
||||
return await fetchJsonOrTRPCThrow<TOut, TPostBody>({ url, method: 'POST', headers, body, name: 'Ollama' });
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export const llmOllamaRouter = createTRPCRouter({
|
||||
|
||||
// fetch as a large text buffer, made of JSONs separated by newlines
|
||||
const { headers, url } = ollamaAccess(input.access, '/api/pull');
|
||||
const pullRequest = await fetchTextOrTRPCError(url, 'POST', headers, { 'name': input.name }, 'Ollama::pull');
|
||||
const pullRequest = await fetchTextOrTRPCThrow({ url, method: 'POST', headers, body: { 'name': input.name }, name: 'Ollama::pull' });
|
||||
|
||||
// accumulate status and error messages
|
||||
let lastStatus: string = 'unknown';
|
||||
@@ -183,7 +183,7 @@ export const llmOllamaRouter = createTRPCRouter({
|
||||
.input(adminPullModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { headers, url } = ollamaAccess(input.access, '/api/delete');
|
||||
const deleteOutput = await fetchTextOrTRPCError(url, 'DELETE', headers, { 'name': input.name }, 'Ollama::delete');
|
||||
const deleteOutput = await fetchTextOrTRPCThrow({ url, method: 'DELETE', headers, body: { 'name': input.name }, name: 'Ollama::delete' });
|
||||
if (deleteOutput?.length && deleteOutput !== 'null')
|
||||
throw new Error('Ollama delete issue: ' + deleteOutput);
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { T2iCreateImageOutput, t2iCreateImagesOutputSchema } from '~/modules/t2i/t2i.server';
|
||||
|
||||
@@ -652,12 +652,12 @@ export function openAIChatCompletionPayload(dialect: OpenAIDialects, model: Open
|
||||
|
||||
async function openaiGETOrThrow<TOut extends object>(access: OpenAIAccessSchema, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = openAIAccess(access, null, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut>(url, 'GET', headers, undefined, `OpenAI/${access.dialect}`);
|
||||
return await fetchJsonOrTRPCThrow<TOut>({ url, headers, name: `OpenAI/${access.dialect}` });
|
||||
}
|
||||
|
||||
async function openaiPOSTOrThrow<TOut extends object, TPostBody extends object>(access: OpenAIAccessSchema, modelRefId: string | null, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = openAIAccess(access, modelRefId, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut, TPostBody>(url, 'POST', headers, body, `OpenAI/${access.dialect}`);
|
||||
return await fetchJsonOrTRPCThrow<TOut, TPostBody>({ url, method: 'POST', headers, body, name: `OpenAI/${access.dialect}` });
|
||||
}
|
||||
|
||||
function parseChatGenerateFCOutput(isFunctionsCall: boolean, message: OpenAIWire.ChatCompletion.ResponseFunctionCall) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { getPngDimensions, t2iCreateImagesOutputSchema } from '../t2i.server';
|
||||
|
||||
@@ -115,8 +115,8 @@ export const prodiaRouter = createTRPCRouter({
|
||||
// fetch in parallel both the SD and SDXL models
|
||||
const { headers, url } = prodiaAccess(input.prodiaKey, `/v1/sd/models`);
|
||||
const [sdModelIds, sdXlModelIds] = await Promise.all([
|
||||
fetchJsonOrTRPCError<string[]>(url, 'GET', headers, undefined, 'Prodia'),
|
||||
fetchJsonOrTRPCError<string[]>(url.replace('/sd/', '/sdxl/'), 'GET', headers, undefined, 'Prodia'),
|
||||
fetchJsonOrTRPCThrow<string[]>({ url, headers, name: 'Prodia SD' }),
|
||||
fetchJsonOrTRPCThrow<string[]>({ url: url.replace('/sd/', '/sdxl/'), headers, name: 'Prodia SDXL' }),
|
||||
]);
|
||||
const apiModelIDs = [...sdModelIds, ...sdXlModelIds];
|
||||
|
||||
@@ -193,12 +193,12 @@ export interface JobResponse {
|
||||
|
||||
async function createGenerationJob<TJobRequest extends JobRequestBase>(apiKey: string | undefined, isGenSDXL: boolean, jobRequest: TJobRequest): Promise<JobResponse> {
|
||||
const { headers, url } = prodiaAccess(apiKey, isGenSDXL ? '/v1/sdxl/generate' : '/v1/sd/generate');
|
||||
return await fetchJsonOrTRPCError<JobResponse, TJobRequest>(url, 'POST', headers, jobRequest, 'Prodia Job Create');
|
||||
return await fetchJsonOrTRPCThrow<JobResponse, TJobRequest>({ url, method: 'POST', headers, body: jobRequest, name: 'Prodia Job Create' });
|
||||
}
|
||||
|
||||
async function getJobStatus(apiKey: string | undefined, jobId: string): Promise<JobResponse> {
|
||||
const { headers, url } = prodiaAccess(apiKey, `/v1/job/${jobId}`);
|
||||
return await fetchJsonOrTRPCError<JobResponse>(url, 'GET', headers, undefined, 'Prodia Job Status');
|
||||
return await fetchJsonOrTRPCThrow<JobResponse>({ url, headers, name: 'Prodia Job Status' });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchJsonOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
|
||||
export const publishToInputSchema = z.object({
|
||||
@@ -55,7 +55,13 @@ export async function postToPasteGGOrThrow(title: string, fileName: string, file
|
||||
}],
|
||||
};
|
||||
|
||||
return await fetchJsonOrTRPCError<PasteGGWire.PasteResponse, PasteGGWire.PasteRequest>('https://api.paste.gg/v1/pastes', 'POST', { 'Content-Type': 'application/json' }, pasteData, 'PasteGG');
|
||||
return await fetchJsonOrTRPCThrow<PasteGGWire.PasteResponse, PasteGGWire.PasteRequest>({
|
||||
url: 'https://api.paste.gg/v1/pastes',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: pasteData,
|
||||
name: 'PasteGG',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { fetchTextOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchTextOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { chatGptParseConversation, chatGptSharedChatSchema } from './chatgpt';
|
||||
import { postToPasteGGOrThrow, publishToInputSchema, publishToOutputSchema } from './pastegg';
|
||||
@@ -34,12 +34,16 @@ export const tradeRouter = createTRPCRouter({
|
||||
htmlPage = input.htmlPage;
|
||||
} else {
|
||||
// add headers that make it closest to a browser request
|
||||
htmlPage = await fetchTextOrTRPCError(input.url, 'GET', {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36',
|
||||
}, undefined, 'ChatGPT Importer');
|
||||
htmlPage = await fetchTextOrTRPCThrow({
|
||||
url: input.url,
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36',
|
||||
},
|
||||
name: 'ChatGPT Importer',
|
||||
});
|
||||
}
|
||||
|
||||
const data = chatGptParseConversation(htmlPage);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { fetchTextOrTRPCError } from '~/server/api/trpc.router.fetchers';
|
||||
import { fetchTextOrTRPCThrow } from '~/server/api/trpc.router.fetchers';
|
||||
|
||||
import { fetchYouTubeTranscript } from './youtube.fetcher';
|
||||
|
||||
@@ -24,7 +24,7 @@ export const youtubeRouter = createTRPCRouter({
|
||||
.input(inputSchema)
|
||||
.query(async ({ input }) => {
|
||||
const { videoId } = input;
|
||||
return await fetchYouTubeTranscript(videoId, url => fetchTextOrTRPCError(url, 'GET', {}, undefined, 'YouTube Transcript'));
|
||||
return await fetchYouTubeTranscript(videoId, (url) => fetchTextOrTRPCThrow({ url, name: 'YouTube Transcript' }));
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
@@ -3,78 +3,134 @@ import { TRPCError } from '@trpc/server';
|
||||
import { debugGenerateCurlCommand, safeErrorString, SERVER_DEBUG_WIRE } from '~/server/wire';
|
||||
|
||||
|
||||
//
|
||||
// NOTE: This file is used in the server-side code, and not in the client-side code.
|
||||
//
|
||||
// It is used to fetch data from external APIs, and throw TRPC errors on failure.
|
||||
//
|
||||
// It handles connection errors, HTTP errors, and parsing errors.
|
||||
//
|
||||
|
||||
// JSON fetcher
|
||||
export const fetchJsonOrTRPCError: <TOut extends object, TPostBody extends object | undefined = undefined /* undefined for GET requests */>(
|
||||
url: string,
|
||||
method: 'GET' | 'POST',
|
||||
headers: HeadersInit,
|
||||
body: TPostBody,
|
||||
moduleName: string,
|
||||
) => Promise<TOut> = createFetcherFromTRPC(async (response) => await response.json(), 'json');
|
||||
export async function fetchJsonOrTRPCThrow<TOut extends object = object, TBody extends object | undefined = undefined>(config: RequestConfig<TBody>): Promise<TOut> {
|
||||
return _fetchFromTRPC<TBody, TOut>(config, async (response) => await response.json(), 'json');
|
||||
}
|
||||
|
||||
// Text fetcher
|
||||
export const fetchTextOrTRPCError: <TPostBody extends object | undefined>(
|
||||
url: string,
|
||||
method: 'GET' | 'POST' | 'DELETE',
|
||||
headers: HeadersInit,
|
||||
body: TPostBody,
|
||||
moduleName: string,
|
||||
) => Promise<string> = createFetcherFromTRPC(async (response) => await response.text(), 'text');
|
||||
|
||||
|
||||
// internal safe fetch implementation
|
||||
function createFetcherFromTRPC<TPostBody, TOut>(parser: (response: Response) => Promise<TOut>, parserName: string): (url: string, method: 'GET' | 'POST' | 'DELETE', headers: HeadersInit, body: TPostBody | undefined, moduleName: string) => Promise<TOut> {
|
||||
return async (url, method, headers, body, moduleName) => {
|
||||
// Fetch
|
||||
let response: Response;
|
||||
try {
|
||||
if (SERVER_DEBUG_WIRE)
|
||||
console.log('-> tRPC', debugGenerateCurlCommand(method, url, headers, body as any));
|
||||
|
||||
response = await fetch(url, { method, headers, ...(body !== undefined ? { body: JSON.stringify(body) } : {}) });
|
||||
} catch (error: any) {
|
||||
const errorCause: object | undefined = error ? error?.cause ?? undefined : undefined;
|
||||
console.error(`[${method}] ${moduleName} error (fetch):`, errorCause || error /* circular struct, don't use JSON.stringify.. */);
|
||||
// HTTP 400
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `[Issue] ${moduleName}: (network): ${safeErrorString(error) || 'unknown fetch error'}`
|
||||
+ (errorCause ? ` - ${errorCause?.toString()}` : '')
|
||||
+ ((errorCause && (errorCause as any)?.code === 'ECONNREFUSED') ? ` - is "${url}" accessible by the server?` : ''),
|
||||
cause: errorCause,
|
||||
});
|
||||
}
|
||||
|
||||
/* Check for non-200s
|
||||
* These are the MOST FREQUENT errors, application level response. Such as:
|
||||
* - 400 when requesting an invalid size to Dall-E3, etc..
|
||||
*/
|
||||
if (!response.ok) {
|
||||
let payload: any | null = await response.json().catch(() => null);
|
||||
if (payload === null)
|
||||
payload = await response.text().catch(() => null);
|
||||
console.error(`[${method}] ${moduleName} error (upstream):`, response.status, response.statusText, payload);
|
||||
// HTTP 400
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `[Issue] ${moduleName}: ${response.statusText}` // (${response.status})`
|
||||
+ (payload ? ` - ${safeErrorString(payload)}` : '')
|
||||
+ (response.status === 403 ? ` - is "${url}" accessible by the server?` : '')
|
||||
+ (response.status === 404 ? ` - "${url}" cannot be found by the server` : '')
|
||||
+ (response.status === 502 ? ` - is "${url}" not available?` : ''),
|
||||
});
|
||||
}
|
||||
|
||||
// Safe Parse
|
||||
try {
|
||||
return await parser(response);
|
||||
} catch (error: any) {
|
||||
console.error(`[${method}] ${moduleName} error (parse):`, error);
|
||||
// HTTP 422
|
||||
throw new TRPCError({
|
||||
code: 'UNPROCESSABLE_CONTENT',
|
||||
message: `[Issue] ${moduleName}: (parsing): ${safeErrorString(error) || `Unknown ${parserName} parsing error`}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
export async function fetchTextOrTRPCThrow<TBody extends object | undefined = undefined>(config: RequestConfig<TBody>): Promise<string> {
|
||||
return _fetchFromTRPC<TBody, string>(config, async (response) => await response.text(), 'text');
|
||||
}
|
||||
|
||||
// Response fetcher
|
||||
export async function fetchResponseOrTRPCThrow<TBody extends object | undefined = undefined>(config: RequestConfig<TBody>): Promise<Response> {
|
||||
return _fetchFromTRPC<TBody, Response>(config, async (response) => response, 'response');
|
||||
}
|
||||
|
||||
|
||||
type RequestConfig<TBody extends object | undefined> = {
|
||||
url: string;
|
||||
headers?: HeadersInit;
|
||||
signal?: AbortSignal;
|
||||
name: string;
|
||||
} & (
|
||||
| { method?: 'GET' /* in case of GET, the method is optional, and no body */ }
|
||||
| { method: 'POST'; body: TBody }
|
||||
| { method: 'DELETE'; body: TBody }
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* Internal fetcher
|
||||
* - Parses errors on connection, http responses, and parsing
|
||||
* - Throws TRPCErrors (as this is used within tRPC procedures)
|
||||
*/
|
||||
async function _fetchFromTRPC<TBody extends object | undefined, TOut>(
|
||||
config: RequestConfig<TBody>,
|
||||
responseParser: (response: Response) => Promise<TOut>,
|
||||
parserName: 'json' | 'text' | 'response',
|
||||
): Promise<TOut> {
|
||||
|
||||
const { url, method = 'GET', headers, name: moduleName, signal } = config;
|
||||
const body = 'body' in config ? config.body : undefined;
|
||||
|
||||
// 1. Fetch a Response object
|
||||
let response: Response;
|
||||
try {
|
||||
|
||||
if (SERVER_DEBUG_WIRE)
|
||||
console.log('-> tRPC', debugGenerateCurlCommand(method, url, headers, body as any));
|
||||
|
||||
// upstream request
|
||||
const request: RequestInit = { method };
|
||||
if (headers !== undefined) request.headers = headers;
|
||||
if (body !== undefined) request.body = JSON.stringify(body);
|
||||
if (signal) request.signal = signal;
|
||||
|
||||
// upstream fetch
|
||||
response = await fetch(url, request);
|
||||
|
||||
} catch (error: any) {
|
||||
|
||||
// [logging - Connection error] candidate for the logging system
|
||||
const errorCause: object | undefined = error ? error?.cause ?? undefined : undefined;
|
||||
console.error(`[${method}] ${moduleName} error (network):`, errorCause || error /* circular struct, don't use JSON.stringify.. */);
|
||||
|
||||
// Handle Connection errors - HTTP 400
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `[${moduleName} network issue]: ${safeErrorString(error) || 'unknown fetch error'}`
|
||||
+ (errorCause
|
||||
? ` - ${errorCause?.toString()}`
|
||||
: '')
|
||||
+ ((errorCause && (errorCause as any)?.code === 'ECONNREFUSED')
|
||||
? ` - is "${url}" accessible by the server?`
|
||||
: ''),
|
||||
cause: errorCause,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Check for non-200s
|
||||
// These are the MOST FREQUENT errors, application level response. Such as:
|
||||
// - 400 when requesting an invalid size to Dall-E3, etc..
|
||||
// - 403 when requesting a localhost URL from a public server, etc..
|
||||
if (!response.ok) {
|
||||
// try to parse a json or text payload, which frequently contains the error, if present
|
||||
let payload: any | null = await response.json().catch(() => null);
|
||||
if (payload === null)
|
||||
payload = await response.text().catch(() => null);
|
||||
|
||||
// [logging - HTTP error] candidate for the logging system
|
||||
console.error(`[${method}] ${moduleName} error (upstream):`, response.status, response.statusText, payload);
|
||||
|
||||
// HTTP 400
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `[${moduleName} issue]: ${response.statusText}`
|
||||
+ (payload
|
||||
? ` - ${safeErrorString(payload)}` : '')
|
||||
+ (response.status === 403
|
||||
? ` - is "${url}" accessible by the server?` : '')
|
||||
+ (response.status === 404
|
||||
? ` - "${url}" cannot be found by the server` : '')
|
||||
+ (response.status === 502 ?
|
||||
` - is "${url}" not available?` : ''),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Safe Parse
|
||||
let value: TOut;
|
||||
try {
|
||||
value = await responseParser(response);
|
||||
} catch (error: any) {
|
||||
// [logging - Parsing error] candidate for the logging system
|
||||
console.error(`[${method}] ${moduleName} error (parse, ${parserName}):`, error);
|
||||
|
||||
// HTTP 422
|
||||
throw new TRPCError({
|
||||
code: 'UNPROCESSABLE_CONTENT',
|
||||
message: `[${moduleName} parsing issue]: ${safeErrorString(error) || 'unknown error'}`,
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
+2
-2
@@ -79,10 +79,10 @@ export function serverCapitalizeFirstLetter(string: string) {
|
||||
/**
|
||||
* Weak (meaning the string could be encoded poorly) function that returns a string that can be used to debug a request
|
||||
*/
|
||||
export function debugGenerateCurlCommand(method: 'GET' | 'POST' | 'DELETE', url: string, headers: HeadersInit, body: object | undefined): string {
|
||||
export function debugGenerateCurlCommand(method: 'GET' | 'POST' | 'DELETE', url: string, headers?: HeadersInit, body?: object): string {
|
||||
let curl = `curl -X ${method} '${url}' `;
|
||||
|
||||
const headersRecord = headers as Record<string, string>;
|
||||
const headersRecord = (headers || {}) as Record<string, string>;
|
||||
|
||||
for (const header in headersRecord)
|
||||
curl += `-H '${header}: ${headersRecord[header]}' `;
|
||||
|
||||
Reference in New Issue
Block a user