Skip to content

Commit 8e71b13

Browse files
committed
refactor: disable next client error events route after fastify cutover
1 parent ef68990 commit 8e71b13

File tree

3 files changed

+81
-159
lines changed

3 files changed

+81
-159
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/** @jest-environment node */
2+
import { REQUEST_ID_HEADER } from '@lib/errors/app-error';
3+
4+
import { GET, POST } from './route';
5+
6+
describe('Next internal error-events client route disabled stub', () => {
7+
it('returns 503 envelope for POST and preserves request id header', async () => {
8+
const response = await POST(
9+
new Request('http://localhost/api/internal/error-events/client', {
10+
method: 'POST',
11+
headers: {
12+
'x-request-id': 'next-disabled-client-error-post',
13+
},
14+
})
15+
);
16+
const payload = await response.json();
17+
18+
expect(response.status).toBe(503);
19+
expect(response.headers.get('x-agentifui-next-handler')).toBe(
20+
'next-disabled'
21+
);
22+
expect(response.headers.get(REQUEST_ID_HEADER)).toBe(
23+
'next-disabled-client-error-post'
24+
);
25+
expect(payload.success).toBe(false);
26+
expect(payload.app_error?.code).toBe('NEXT_BUSINESS_ROUTE_DISABLED');
27+
expect(payload.request_id).toBe('next-disabled-client-error-post');
28+
});
29+
30+
it('reuses POST behavior for GET', async () => {
31+
const response = await GET(
32+
new Request('http://localhost/api/internal/error-events/client', {
33+
method: 'GET',
34+
})
35+
);
36+
const payload = await response.json();
37+
38+
expect(response.status).toBe(503);
39+
expect(response.headers.get('x-agentifui-next-handler')).toBe(
40+
'next-disabled'
41+
);
42+
expect(response.headers.get(REQUEST_ID_HEADER)).toBeTruthy();
43+
expect(payload.app_error?.code).toBe('NEXT_BUSINESS_ROUTE_DISABLED');
44+
});
45+
});
Lines changed: 35 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,185 +1,62 @@
1-
import { resolveSessionIdentityReadOnly } from '@lib/auth/better-auth/session-identity';
21
import {
32
REQUEST_ID_HEADER,
43
buildAppErrorDetail,
54
buildAppErrorEnvelope,
65
resolveRequestId,
76
} from '@lib/errors/app-error';
8-
import { recordErrorEvent } from '@lib/server/errors/error-events';
97

108
import { NextResponse } from 'next/server';
119

1210
export const runtime = 'nodejs';
1311

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) {
7113
const detail = buildAppErrorDetail({
72-
status: input.status,
14+
status,
7315
source: 'next-api',
74-
code: input.code,
75-
userMessage: input.userMessage,
76-
developerMessage: input.developerMessage,
7716
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,
7823
});
7924

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+
});
8935
response.headers.set(REQUEST_ID_HEADER, requestId);
36+
response.headers.set('x-agentifui-next-handler', 'next-disabled');
9037
return response;
9138
}
9239

9340
export async function POST(request: Request) {
94-
const fallbackRequestId = resolveRequestId(request);
41+
return buildDisabledJson(503, resolveRequestId(request));
42+
}
9543

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+
}
10747

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+
}
12151

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+
}
13855

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+
}
16059

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);
18562
}

scripts/fastify-cutover-off.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ client_error_status=$(curl -sS -o /tmp/agentifui-cutover-off-client-error.json -
5252
-H 'content-type: application/json' \
5353
--data '{}')
5454

55-
if [[ "${client_error_status}" != "400" ]]; then
55+
if [[ "${client_error_status}" != "503" ]]; then
5656
echo "[cutover-off] client error route smoke failed: status=${client_error_status}"
5757
cat /tmp/agentifui-cutover-off-client-error.json || true
5858
exit 1

0 commit comments

Comments
 (0)