diff --git a/src/modules/backend/backend.router.ts b/src/modules/backend/backend.router.ts index 2d39c1030..ad83a90f7 100644 --- a/src/modules/backend/backend.router.ts +++ b/src/modules/backend/backend.router.ts @@ -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', + }); }), }); diff --git a/src/modules/elevenlabs/elevenlabs.router.ts b/src/modules/elevenlabs/elevenlabs.router.ts index 874b60649..5d77ef22f 100644 --- a/src/modules/elevenlabs/elevenlabs.router.ts +++ b/src/modules/elevenlabs/elevenlabs.router.ts @@ -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(url, 'GET', headers, undefined, 'ElevenLabs'); + const voicesList = await fetchJsonOrTRPCThrow({ + url, + headers, + name: 'ElevenLabs', + }); // bring category != 'premade' to the top voicesList.voices.sort((a, b) => { diff --git a/src/modules/google/search.router.ts b/src/modules/google/search.router.ts index 10501099d..05ebbed73 100644 --- a/src/modules/google/search.router.ts +++ b/src/modules/google/search.router.ts @@ -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', diff --git a/src/modules/llms/server/anthropic/anthropic.router.ts b/src/modules/llms/server/anthropic/anthropic.router.ts index 169023626..b800cd4b0 100644 --- a/src/modules/llms/server/anthropic/anthropic.router.ts +++ b/src/modules/llms/server/anthropic/anthropic.router.ts @@ -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(access: AnthropicAccessSchema, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise { const { headers, url } = anthropicAccess(access, apiPath); - return await fetchJsonOrTRPCError(url, 'POST', headers, body, 'Anthropic'); + return await fetchJsonOrTRPCThrow({ url, method: 'POST', headers, body, name: 'Anthropic' }); } export function anthropicAccess(access: AnthropicAccessSchema, apiPath: string): { headers: HeadersInit, url: string } { diff --git a/src/modules/llms/server/gemini/gemini.router.ts b/src/modules/llms/server/gemini/gemini.router.ts index 2e4cd0c46..cdc6fecd8 100644 --- a/src/modules/llms/server/gemini/gemini.router.ts +++ b/src/modules/llms/server/gemini/gemini.router.ts @@ -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(access: GeminiAccessSchema, modelRefId: string | null, apiPath: string /*, signal?: AbortSignal*/): Promise { const { headers, url } = geminiAccess(access, modelRefId, apiPath); - return await fetchJsonOrTRPCError(url, 'GET', headers, undefined, 'Gemini'); + return await fetchJsonOrTRPCThrow({ url, headers, name: 'Gemini' }); } async function geminiPOST(access: GeminiAccessSchema, modelRefId: string | null, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise { const { headers, url } = geminiAccess(access, modelRefId, apiPath); - return await fetchJsonOrTRPCError(url, 'POST', headers, body, 'Gemini'); + return await fetchJsonOrTRPCThrow({ url, method: 'POST', headers, body, name: 'Gemini' }); } diff --git a/src/modules/llms/server/ollama/ollama.router.ts b/src/modules/llms/server/ollama/ollama.router.ts index e8097d06c..c85b317c9 100644 --- a/src/modules/llms/server/ollama/ollama.router.ts +++ b/src/modules/llms/server/ollama/ollama.router.ts @@ -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(access: OllamaAccessSchema, apiPath: string /*, signal?: AbortSignal*/): Promise { const { headers, url } = ollamaAccess(access, apiPath); - return await fetchJsonOrTRPCError(url, 'GET', headers, undefined, 'Ollama'); + return await fetchJsonOrTRPCThrow({ url, headers, name: 'Ollama' }); } async function ollamaPOST(access: OllamaAccessSchema, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise { const { headers, url } = ollamaAccess(access, apiPath); - return await fetchJsonOrTRPCError(url, 'POST', headers, body, 'Ollama'); + return await fetchJsonOrTRPCThrow({ 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); }), diff --git a/src/modules/llms/server/openai/openai.router.ts b/src/modules/llms/server/openai/openai.router.ts index 13ca7dabb..38585f6e7 100644 --- a/src/modules/llms/server/openai/openai.router.ts +++ b/src/modules/llms/server/openai/openai.router.ts @@ -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(access: OpenAIAccessSchema, apiPath: string /*, signal?: AbortSignal*/): Promise { const { headers, url } = openAIAccess(access, null, apiPath); - return await fetchJsonOrTRPCError(url, 'GET', headers, undefined, `OpenAI/${access.dialect}`); + return await fetchJsonOrTRPCThrow({ url, headers, name: `OpenAI/${access.dialect}` }); } async function openaiPOSTOrThrow(access: OpenAIAccessSchema, modelRefId: string | null, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise { const { headers, url } = openAIAccess(access, modelRefId, apiPath); - return await fetchJsonOrTRPCError(url, 'POST', headers, body, `OpenAI/${access.dialect}`); + return await fetchJsonOrTRPCThrow({ url, method: 'POST', headers, body, name: `OpenAI/${access.dialect}` }); } function parseChatGenerateFCOutput(isFunctionsCall: boolean, message: OpenAIWire.ChatCompletion.ResponseFunctionCall) { diff --git a/src/modules/t2i/prodia/prodia.router.ts b/src/modules/t2i/prodia/prodia.router.ts index 4edba3d92..529a62ec9 100644 --- a/src/modules/t2i/prodia/prodia.router.ts +++ b/src/modules/t2i/prodia/prodia.router.ts @@ -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(url, 'GET', headers, undefined, 'Prodia'), - fetchJsonOrTRPCError(url.replace('/sd/', '/sdxl/'), 'GET', headers, undefined, 'Prodia'), + fetchJsonOrTRPCThrow({ url, headers, name: 'Prodia SD' }), + fetchJsonOrTRPCThrow({ url: url.replace('/sd/', '/sdxl/'), headers, name: 'Prodia SDXL' }), ]); const apiModelIDs = [...sdModelIds, ...sdXlModelIds]; @@ -193,12 +193,12 @@ export interface JobResponse { async function createGenerationJob(apiKey: string | undefined, isGenSDXL: boolean, jobRequest: TJobRequest): Promise { const { headers, url } = prodiaAccess(apiKey, isGenSDXL ? '/v1/sdxl/generate' : '/v1/sd/generate'); - return await fetchJsonOrTRPCError(url, 'POST', headers, jobRequest, 'Prodia Job Create'); + return await fetchJsonOrTRPCThrow({ url, method: 'POST', headers, body: jobRequest, name: 'Prodia Job Create' }); } async function getJobStatus(apiKey: string | undefined, jobId: string): Promise { const { headers, url } = prodiaAccess(apiKey, `/v1/job/${jobId}`); - return await fetchJsonOrTRPCError(url, 'GET', headers, undefined, 'Prodia Job Status'); + return await fetchJsonOrTRPCThrow({ url, headers, name: 'Prodia Job Status' }); } diff --git a/src/modules/trade/server/pastegg.ts b/src/modules/trade/server/pastegg.ts index d7904cd0d..6713a802c 100644 --- a/src/modules/trade/server/pastegg.ts +++ b/src/modules/trade/server/pastegg.ts @@ -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('https://api.paste.gg/v1/pastes', 'POST', { 'Content-Type': 'application/json' }, pasteData, 'PasteGG'); + return await fetchJsonOrTRPCThrow({ + url: 'https://api.paste.gg/v1/pastes', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: pasteData, + name: 'PasteGG', + }); } diff --git a/src/modules/trade/server/trade.router.ts b/src/modules/trade/server/trade.router.ts index d60753fb6..b5975c9e7 100644 --- a/src/modules/trade/server/trade.router.ts +++ b/src/modules/trade/server/trade.router.ts @@ -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); diff --git a/src/modules/youtube/youtube.router.ts b/src/modules/youtube/youtube.router.ts index b43caecac..203771683 100644 --- a/src/modules/youtube/youtube.router.ts +++ b/src/modules/youtube/youtube.router.ts @@ -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' })); }), }); diff --git a/src/server/api/trpc.router.fetchers.ts b/src/server/api/trpc.router.fetchers.ts index 4755a8bbe..1c29d0b9d 100644 --- a/src/server/api/trpc.router.fetchers.ts +++ b/src/server/api/trpc.router.fetchers.ts @@ -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: ( - url: string, - method: 'GET' | 'POST', - headers: HeadersInit, - body: TPostBody, - moduleName: string, -) => Promise = createFetcherFromTRPC(async (response) => await response.json(), 'json'); +export async function fetchJsonOrTRPCThrow(config: RequestConfig): Promise { + return _fetchFromTRPC(config, async (response) => await response.json(), 'json'); +} // Text fetcher -export const fetchTextOrTRPCError: ( - url: string, - method: 'GET' | 'POST' | 'DELETE', - headers: HeadersInit, - body: TPostBody, - moduleName: string, -) => Promise = createFetcherFromTRPC(async (response) => await response.text(), 'text'); - - -// internal safe fetch implementation -function createFetcherFromTRPC(parser: (response: Response) => Promise, parserName: string): (url: string, method: 'GET' | 'POST' | 'DELETE', headers: HeadersInit, body: TPostBody | undefined, moduleName: string) => Promise { - 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(config: RequestConfig): Promise { + return _fetchFromTRPC(config, async (response) => await response.text(), 'text'); +} + +// Response fetcher +export async function fetchResponseOrTRPCThrow(config: RequestConfig): Promise { + return _fetchFromTRPC(config, async (response) => response, 'response'); +} + + +type RequestConfig = { + 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( + config: RequestConfig, + responseParser: (response: Response) => Promise, + parserName: 'json' | 'text' | 'response', +): Promise { + + 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; } diff --git a/src/server/wire.ts b/src/server/wire.ts index cd91d1b33..1c7ae1f2c 100644 --- a/src/server/wire.ts +++ b/src/server/wire.ts @@ -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; + const headersRecord = (headers || {}) as Record; for (const header in headersRecord) curl += `-H '${header}: ${headersRecord[header]}' `;