story-research-zapwall/lib/api/nip95-upload/responseFormatting.ts
2026-01-13 17:23:28 +01:00

133 lines
5.8 KiB
TypeScript

import type { NextApiResponse } from 'next'
import type { ProxyUploadResponse } from './types'
export function handleProxyResponse(params: { res: NextApiResponse; response: ProxyUploadResponse; targetEndpoint: string }): void {
if (params.response.statusCode < 200 || params.response.statusCode >= 300) {
respondNonOk({ res: params.res, response: params.response, targetEndpoint: params.targetEndpoint })
return
}
if (isHtmlResponse(params.response.body)) {
respondHtml({ res: params.res, response: params.response, targetEndpoint: params.targetEndpoint })
return
}
const parseResult = parseJsonSafe(params.response.body)
if (!parseResult.ok) {
console.error('NIP-95 proxy JSON parse error:', { targetEndpoint: params.targetEndpoint, bodyPreview: params.response.body.substring(0, 100) })
params.res.status(500).json({ error: 'Invalid upload response: Invalid JSON response. The endpoint may not be a valid NIP-95 upload endpoint.' })
return
}
params.res.status(200).json(parseResult.value)
}
function respondNonOk(params: { res: NextApiResponse; response: ProxyUploadResponse; targetEndpoint: string }): void {
const errorText = params.response.body.substring(0, 200)
console.error('NIP-95 proxy response error:', {
targetEndpoint: params.targetEndpoint,
finalUrl: params.response.finalUrl,
status: params.response.statusCode,
statusText: params.response.statusMessage,
errorText,
})
params.res.status(params.response.statusCode).json({ error: buildUserFriendlyHttpError(params.response.statusCode, params.response.statusMessage, errorText) })
}
function buildUserFriendlyHttpError(statusCode: number, statusMessage: string, errorText: string): string {
if (statusCode === 401) {
return 'Authentication required. This endpoint requires authorization headers.'
}
if (statusCode === 403) {
return 'Access forbidden. This endpoint may require authentication or have restrictions.'
}
if (statusCode === 405) {
return 'Method not allowed. This endpoint may not support POST requests or the URL may be incorrect.'
}
if (statusCode === 413) {
return 'File too large. The file exceeds the maximum size allowed by this endpoint.'
}
if (statusCode >= 500) {
return `Server error (${statusCode}). The endpoint server encountered an error.`
}
return errorText || `Upload failed: ${statusCode} ${statusMessage}`
}
export function isHtmlResponse(body: string): boolean {
const trimmedBody = body.trim()
return trimmedBody.startsWith('<!DOCTYPE') || trimmedBody.startsWith('<html') || trimmedBody.startsWith('<!')
}
function respondHtml(params: { res: NextApiResponse; response: ProxyUploadResponse; targetEndpoint: string }): void {
const title = readHtmlTitle(params.response.body)
const h1 = readHtmlH1(params.response.body)
const errorText = title ?? h1 ?? 'HTML error page returned'
const flags = detectHtmlErrorFlags({ body: params.response.body, title })
console.error('NIP-95 proxy HTML response error:', {
targetEndpoint: params.targetEndpoint,
finalUrl: params.response.finalUrl,
status: params.response.statusCode,
errorText,
is404: flags.is404,
is403: flags.is403,
is500: flags.is500,
bodyPreview: params.response.body.substring(0, 500),
contentType: 'HTML (expected JSON)',
suggestion: buildHtmlErrorSuggestion(flags),
})
params.res.status(500).json({ error: buildHtmlUserMessage({ ...flags, finalUrl: params.response.finalUrl, errorText }) })
}
function buildHtmlUserMessage(params: { is404: boolean; is403: boolean; is500: boolean; finalUrl: string; errorText: string }): string {
if (params.is404) {
return `Endpoint not found (404). The URL may be incorrect: ${params.finalUrl}`
}
if (params.is403) {
return 'Access forbidden (403). The endpoint may require authentication or have restrictions.'
}
if (params.is500) {
return 'Server error (500). The endpoint server encountered an error.'
}
return `Endpoint returned an HTML error page instead of JSON. The endpoint may be unavailable, the URL may be incorrect, or specific headers may be required. Error: ${params.errorText}`
}
type JsonParseResult = { ok: true; value: unknown } | { ok: false }
function parseJsonSafe(body: string): JsonParseResult {
try {
return { ok: true, value: JSON.parse(body) as unknown }
} catch {
return { ok: false }
}
}
function readHtmlTitle(body: string): string | undefined {
return body.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]
}
function readHtmlH1(body: string): string | undefined {
return body.match(/<h1[^>]*>([^<]+)<\/h1>/i)?.[1]
}
function detectHtmlErrorFlags(params: { body: string; title: string | undefined }): { is404: boolean; is403: boolean; is500: boolean } {
const is404 = isHtmlErrorForCode({ body: params.body, title: params.title, code: '404', marker: 'Not Found' })
const is403 = isHtmlErrorForCode({ body: params.body, title: params.title, code: '403', marker: 'Forbidden' })
const is500 = isHtmlErrorForCode({ body: params.body, title: params.title, code: '500', marker: 'Internal Server Error' })
return { is404, is403, is500 }
}
function isHtmlErrorForCode(params: { body: string; title: string | undefined; code: string; marker: string }): boolean {
const titleHasCode = params.title?.includes(params.code) === true
return params.body.includes(params.code) || params.body.includes(params.marker) || titleHasCode
}
function buildHtmlErrorSuggestion(params: { is404: boolean; is403: boolean; is500: boolean }): string {
if (params.is404) {
return 'The endpoint URL may be incorrect or the endpoint does not exist'
}
if (params.is403) {
return 'The endpoint may require authentication or have restrictions'
}
if (params.is500) {
return 'The endpoint server encountered an internal error'
}
return 'The endpoint returned HTML instead of JSON; verify URL and required headers'
}