Backend Fetchers: improve

This commit is contained in:
Enrico Ros
2024-07-06 05:38:26 -07:00
parent c6973f6b4e
commit 8505ba6b84
13 changed files with 187 additions and 110 deletions
+7 -4
View File
@@ -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',
});
}),
});
+6 -2
View File
@@ -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) => {
+6 -2
View File
@@ -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) {
+5 -5
View File
@@ -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' });
}
+8 -2
View File
@@ -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',
});
}
+11 -7
View File
@@ -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);
+2 -2
View File
@@ -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' }));
}),
});
+127 -71
View File
@@ -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
View File
@@ -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]}' `;