|
1 | | -import { resolveSessionIdentityReadOnly } from '@lib/auth/better-auth/session-identity'; |
2 | 1 | import { |
3 | 2 | REQUEST_ID_HEADER, |
4 | 3 | buildAppErrorDetail, |
5 | 4 | buildAppErrorEnvelope, |
6 | 5 | resolveRequestId, |
7 | 6 | } from '@lib/errors/app-error'; |
8 | | -import { recordErrorEvent } from '@lib/server/errors/error-events'; |
9 | 7 |
|
10 | 8 | import { NextResponse } from 'next/server'; |
11 | 9 |
|
12 | 10 | export const runtime = 'nodejs'; |
13 | 11 |
|
14 | | -type PayloadObject = Record<string, unknown>; |
15 | | - |
16 | | -type SupportedSeverity = 'info' | 'warn' | 'error' | 'critical'; |
17 | | - |
18 | | -function readString(value: unknown): string | null { |
19 | | - if (typeof value !== 'string') { |
20 | | - return null; |
21 | | - } |
22 | | - |
23 | | - const normalized = value.trim(); |
24 | | - return normalized.length > 0 ? normalized.slice(0, 4000) : null; |
25 | | -} |
26 | | - |
27 | | -function readBoolean(value: unknown, fallbackValue: boolean): boolean { |
28 | | - if (typeof value === 'boolean') { |
29 | | - return value; |
30 | | - } |
31 | | - return fallbackValue; |
32 | | -} |
33 | | - |
34 | | -function readNumber(value: unknown): number | null { |
35 | | - if (typeof value !== 'number' || !Number.isFinite(value)) { |
36 | | - return null; |
37 | | - } |
38 | | - return value; |
39 | | -} |
40 | | - |
41 | | -function readObject(value: unknown): PayloadObject { |
42 | | - if (!value || typeof value !== 'object' || Array.isArray(value)) { |
43 | | - return {}; |
44 | | - } |
45 | | - return value as PayloadObject; |
46 | | -} |
47 | | - |
48 | | -function readSeverity(value: unknown): SupportedSeverity { |
49 | | - const normalized = readString(value)?.toLowerCase(); |
50 | | - if ( |
51 | | - normalized === 'info' || |
52 | | - normalized === 'warn' || |
53 | | - normalized === 'error' || |
54 | | - normalized === 'critical' |
55 | | - ) { |
56 | | - return normalized; |
57 | | - } |
58 | | - return 'error'; |
59 | | -} |
60 | | - |
61 | | -function buildErrorResponse( |
62 | | - request: Request, |
63 | | - input: { |
64 | | - status: number; |
65 | | - code: string; |
66 | | - userMessage: string; |
67 | | - developerMessage: string; |
68 | | - } |
69 | | -) { |
70 | | - const requestId = resolveRequestId(request); |
| 12 | +function buildDisabledResponse(status: number, requestId: string) { |
71 | 13 | const detail = buildAppErrorDetail({ |
72 | | - status: input.status, |
| 14 | + status, |
73 | 15 | source: 'next-api', |
74 | | - code: input.code, |
75 | | - userMessage: input.userMessage, |
76 | | - developerMessage: input.developerMessage, |
77 | 16 | requestId, |
| 17 | + code: 'NEXT_BUSINESS_ROUTE_DISABLED', |
| 18 | + userMessage: |
| 19 | + 'This API is served by Fastify. Enable Fastify proxy/cutover to use this endpoint.', |
| 20 | + developerMessage: |
| 21 | + 'Next.js business API route is disabled after Fastify convergence.', |
| 22 | + retryable: false, |
78 | 23 | }); |
79 | 24 |
|
80 | | - const response = NextResponse.json( |
81 | | - buildAppErrorEnvelope(detail, input.userMessage), |
82 | | - { |
83 | | - status: input.status, |
84 | | - headers: { |
85 | | - 'Cache-Control': 'no-store', |
86 | | - }, |
87 | | - } |
88 | | - ); |
| 25 | + return buildAppErrorEnvelope(detail, detail.userMessage); |
| 26 | +} |
| 27 | + |
| 28 | +function buildDisabledJson(status: number, requestId: string) { |
| 29 | + const response = NextResponse.json(buildDisabledResponse(status, requestId), { |
| 30 | + status, |
| 31 | + headers: { |
| 32 | + 'Cache-Control': 'no-store', |
| 33 | + }, |
| 34 | + }); |
89 | 35 | response.headers.set(REQUEST_ID_HEADER, requestId); |
| 36 | + response.headers.set('x-agentifui-next-handler', 'next-disabled'); |
90 | 37 | return response; |
91 | 38 | } |
92 | 39 |
|
93 | 40 | export async function POST(request: Request) { |
94 | | - const fallbackRequestId = resolveRequestId(request); |
| 41 | + return buildDisabledJson(503, resolveRequestId(request)); |
| 42 | +} |
95 | 43 |
|
96 | | - try { |
97 | | - const rawPayload = await request.json(); |
98 | | - const payload = readObject(rawPayload); |
99 | | - if (Object.keys(payload).length === 0) { |
100 | | - return buildErrorResponse(request, { |
101 | | - status: 400, |
102 | | - code: 'CLIENT_ERROR_REPORT_INVALID_PAYLOAD', |
103 | | - userMessage: 'Invalid client error payload', |
104 | | - developerMessage: 'Payload is missing or not an object', |
105 | | - }); |
106 | | - } |
| 44 | +export async function GET(request: Request) { |
| 45 | + return POST(request); |
| 46 | +} |
107 | 47 |
|
108 | | - const userMessage = |
109 | | - readString(payload.userMessage) || |
110 | | - readString(payload.message) || |
111 | | - 'Unexpected client error'; |
112 | | - const developerMessage = |
113 | | - readString(payload.developerMessage) || readString(payload.message); |
114 | | - const requestId = readString(payload.requestId) || fallbackRequestId; |
115 | | - const context = readObject(payload.context); |
116 | | - const route = |
117 | | - readString(payload.route) || |
118 | | - readString(context.pathname) || |
119 | | - readString(context.href) || |
120 | | - '/client'; |
| 48 | +export async function PUT(request: Request) { |
| 49 | + return POST(request); |
| 50 | +} |
121 | 51 |
|
122 | | - let actorUserId: string | undefined; |
123 | | - try { |
124 | | - const resolvedIdentity = await resolveSessionIdentityReadOnly( |
125 | | - request.headers |
126 | | - ); |
127 | | - if (resolvedIdentity.success && resolvedIdentity.data?.userId) { |
128 | | - actorUserId = resolvedIdentity.data.userId; |
129 | | - } |
130 | | - } catch (identityError) { |
131 | | - console.warn( |
132 | | - '[ClientErrorReport] failed to resolve session identity:', |
133 | | - identityError instanceof Error |
134 | | - ? identityError.message |
135 | | - : String(identityError) |
136 | | - ); |
137 | | - } |
| 52 | +export async function PATCH(request: Request) { |
| 53 | + return POST(request); |
| 54 | +} |
138 | 55 |
|
139 | | - await recordErrorEvent({ |
140 | | - code: readString(payload.code) || 'CLIENT_RUNTIME_ERROR', |
141 | | - source: 'frontend', |
142 | | - severity: readSeverity(payload.severity), |
143 | | - retryable: readBoolean(payload.retryable, true), |
144 | | - userMessage, |
145 | | - developerMessage: developerMessage || undefined, |
146 | | - requestId, |
147 | | - traceId: readString(payload.traceId) || undefined, |
148 | | - actorUserId, |
149 | | - httpStatus: readNumber(payload.httpStatus) || undefined, |
150 | | - method: readString(payload.method) || 'CLIENT', |
151 | | - route, |
152 | | - context: { |
153 | | - ...context, |
154 | | - report_origin: 'browser', |
155 | | - server_received_at: new Date().toISOString(), |
156 | | - reporter_user_agent: |
157 | | - request.headers.get('user-agent')?.slice(0, 1000) || null, |
158 | | - }, |
159 | | - }); |
| 56 | +export async function DELETE(request: Request) { |
| 57 | + return POST(request); |
| 58 | +} |
160 | 59 |
|
161 | | - const response = NextResponse.json( |
162 | | - { |
163 | | - success: true, |
164 | | - request_id: requestId, |
165 | | - }, |
166 | | - { |
167 | | - status: 202, |
168 | | - headers: { |
169 | | - 'Cache-Control': 'no-store', |
170 | | - }, |
171 | | - } |
172 | | - ); |
173 | | - response.headers.set(REQUEST_ID_HEADER, requestId); |
174 | | - return response; |
175 | | - } catch (error) { |
176 | | - console.error('[ClientErrorReport] failed to record client error:', error); |
177 | | - return buildErrorResponse(request, { |
178 | | - status: 500, |
179 | | - code: 'CLIENT_ERROR_REPORT_FAILED', |
180 | | - userMessage: 'Failed to record client error', |
181 | | - developerMessage: |
182 | | - error instanceof Error ? error.message : 'Unknown client error report failure', |
183 | | - }); |
184 | | - } |
| 60 | +export async function OPTIONS(request: Request) { |
| 61 | + return POST(request); |
185 | 62 | } |
0 commit comments