mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-20 19:48:52 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78170d2c67 |
@@ -1,6 +1,7 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZDisplayCreateInput } from "@formbricks/types/displays";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -32,7 +33,19 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
const { workspaceId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
let jsonInput;
|
||||
try {
|
||||
jsonInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }, true),
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const inputValidation = ZDisplayCreateInput.safeParse({
|
||||
...jsonInput,
|
||||
workspaceId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { validateSingleUseResponseInput } from "@/app/api/client/[workspaceId]/responses/lib/single-use";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -56,8 +57,14 @@ export const POST = withV1ApiWrapper({
|
||||
const requestHeaders = await headers();
|
||||
let responseInput;
|
||||
try {
|
||||
responseInput = await req.json();
|
||||
responseInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }, true),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: responses.badRequestResponse(
|
||||
"Invalid JSON in request body",
|
||||
@@ -211,7 +218,7 @@ export const POST = withV1ApiWrapper({
|
||||
response: responseData,
|
||||
});
|
||||
|
||||
if (responseInput.finished) {
|
||||
if (responseInputData.finished) {
|
||||
await sendToPipeline({
|
||||
event: "responseFinished",
|
||||
workspaceId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classe
|
||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||
import { handleErrorResponse } from "@/app/api/v1/auth";
|
||||
import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -84,8 +85,14 @@ export const PUT = withV1ApiWrapper({
|
||||
|
||||
let actionClassUpdate;
|
||||
try {
|
||||
actionClassUpdate = await req.json();
|
||||
actionClassUpdate = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { logger } from "@formbricks/logger";
|
||||
import { TActionClass, ZActionClassInput } from "@formbricks/types/action-classes";
|
||||
import { DatabaseError, UniqueConstraintError } from "@formbricks/types/errors";
|
||||
import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -45,8 +46,14 @@ export const POST = withV1ApiWrapper({
|
||||
try {
|
||||
let actionClassInput;
|
||||
try {
|
||||
actionClassInput = await req.json();
|
||||
actionClassInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON input");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { TResponseData, ZResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { handleErrorResponse } from "@/app/api/v1/auth";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { TApiV1Authentication, THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -12,6 +13,11 @@ import { hasPermission } from "@/modules/organization/settings/api-keys/lib/util
|
||||
import { resolveStorageUrlsInObject, validateFileUploads } from "@/modules/storage/utils";
|
||||
import { updateResponseWithQuotaEvaluation } from "./lib/response";
|
||||
|
||||
type TUncheckedResponseUpdate = Record<string, unknown> & {
|
||||
data: TResponseData;
|
||||
language?: string;
|
||||
};
|
||||
|
||||
async function fetchAndAuthorizeResponse(
|
||||
responseId: string,
|
||||
authentication: TApiV1Authentication | undefined,
|
||||
@@ -120,10 +126,16 @@ export const PUT = withV1ApiWrapper({
|
||||
auditLog.oldObject = result.response;
|
||||
}
|
||||
|
||||
let responseUpdate;
|
||||
let responseUpdate: TUncheckedResponseUpdate;
|
||||
try {
|
||||
responseUpdate = await req.json();
|
||||
responseUpdate = await parseJsonBodyWithLimit<TUncheckedResponseUpdate>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -91,8 +92,14 @@ export const POST = withV1ApiWrapper({
|
||||
try {
|
||||
let jsonInput;
|
||||
try {
|
||||
jsonInput = await req.json();
|
||||
jsonInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON input");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { logger } from "@formbricks/logger";
|
||||
import { ZUploadPublicFileRequest } from "@formbricks/types/storage";
|
||||
import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { checkAuth } from "@/app/api/v1/management/storage/lib/utils";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -19,8 +20,14 @@ export const POST = withV1ApiWrapper({
|
||||
let storageInput;
|
||||
|
||||
try {
|
||||
storageInput = await req.json();
|
||||
storageInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON input");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
addLegacyProjectOverwrites,
|
||||
normaliseProjectOverwritesToWorkspace,
|
||||
} from "@/app/lib/api/api-backwards-compat";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import {
|
||||
transformBlocksToQuestions,
|
||||
@@ -22,6 +23,12 @@ import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { resolveStorageUrlsInObject } from "@/modules/storage/utils";
|
||||
|
||||
type TSurveyUpdateBody = Record<string, unknown> & {
|
||||
blocks?: Parameters<typeof validateSurveyInput>[0]["blocks"];
|
||||
endings?: Parameters<typeof transformQuestionsToBlocks>[1];
|
||||
questions?: Parameters<typeof transformQuestionsToBlocks>[0];
|
||||
};
|
||||
|
||||
const fetchAndAuthorizeSurvey = async (
|
||||
surveyId: string,
|
||||
authentication: TAuthenticationApiKey,
|
||||
@@ -164,10 +171,16 @@ export const PUT = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
let surveyUpdate;
|
||||
let surveyUpdate: TSurveyUpdateBody;
|
||||
try {
|
||||
surveyUpdate = await req.json();
|
||||
surveyUpdate = await parseJsonBodyWithLimit<TSurveyUpdateBody>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON input");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
@@ -188,7 +201,7 @@ export const PUT = withV1ApiWrapper({
|
||||
|
||||
if (hasQuestions) {
|
||||
surveyUpdate.blocks = transformQuestionsToBlocks(
|
||||
surveyUpdate.questions,
|
||||
surveyUpdate.questions!,
|
||||
surveyUpdate.endings || result.survey.endings
|
||||
);
|
||||
surveyUpdate.questions = [];
|
||||
@@ -208,7 +221,11 @@ export const PUT = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const featureCheckResult = await checkFeaturePermissions(surveyUpdate, organization, result.survey);
|
||||
const featureCheckResult = await checkFeaturePermissions(
|
||||
surveyUpdate as Parameters<typeof checkFeaturePermissions>[0],
|
||||
organization,
|
||||
result.survey
|
||||
);
|
||||
if (featureCheckResult) {
|
||||
return {
|
||||
response: featureCheckResult,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
addLegacyProjectOverwritesToList,
|
||||
normaliseProjectOverwritesToWorkspace,
|
||||
} from "@/app/lib/api/api-backwards-compat";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import {
|
||||
transformBlocksToQuestions,
|
||||
@@ -84,8 +85,14 @@ export const POST = withV1ApiWrapper({
|
||||
try {
|
||||
let surveyInput;
|
||||
try {
|
||||
surveyInput = await req.json();
|
||||
surveyInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
|
||||
import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { createWebhook, getWebhooks } from "@/app/api/v1/webhooks/lib/webhook";
|
||||
import { ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -40,8 +41,14 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
let webhookInput;
|
||||
try {
|
||||
webhookInput = await req.json();
|
||||
} catch {
|
||||
webhookInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NextRequest } from "next/server";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { TooManyRequestsError } from "@formbricks/types/errors";
|
||||
import { DEFAULT_REQUEST_BODY_LIMIT_BYTES } from "@/app/lib/api/request-body";
|
||||
import { withV3ApiWrapper } from "./api-wrapper";
|
||||
|
||||
const { mockAuthenticateRequest, mockGetServerSession } = vi.hoisted(() => ({
|
||||
@@ -414,6 +415,44 @@ describe("withV3ApiWrapper", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns 413 problem response for oversized JSON input", async () => {
|
||||
const handler = vi.fn(async () => Response.json({ ok: true }));
|
||||
const wrapped = withV3ApiWrapper({
|
||||
auth: "none",
|
||||
schemas: {
|
||||
body: z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
},
|
||||
handler,
|
||||
});
|
||||
|
||||
const response = await wrapped(
|
||||
new NextRequest("http://localhost/api/v3/surveys", {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
headers: {
|
||||
"Content-Length": String(DEFAULT_REQUEST_BODY_LIMIT_BYTES + 1),
|
||||
"Content-Type": "application/json",
|
||||
"x-request-id": "req-payload-too-large",
|
||||
},
|
||||
}),
|
||||
{} as never
|
||||
);
|
||||
|
||||
expect(response.status).toBe(413);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
await expect(response.json()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
code: "payload_too_large",
|
||||
detail: `Request body must not exceed ${DEFAULT_REQUEST_BODY_LIMIT_BYTES} bytes`,
|
||||
requestId: "req-payload-too-large",
|
||||
status: 413,
|
||||
title: "Payload Too Large",
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("returns 400 problem response for invalid route params", async () => {
|
||||
const handler = vi.fn(async () => Response.json({ ok: true }));
|
||||
const wrapped = withV3ApiWrapper({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TooManyRequestsError } from "@formbricks/types/errors";
|
||||
import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { buildAuditLogBaseObject } from "@/app/lib/api/with-api-logging";
|
||||
import { getApiKeyFromHeaders } from "@/modules/api/lib/api-key-auth";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
type InvalidParam,
|
||||
problemBadRequest,
|
||||
problemInternalError,
|
||||
problemPayloadTooLarge,
|
||||
problemTooManyRequests,
|
||||
problemUnauthorized,
|
||||
} from "./response";
|
||||
@@ -170,8 +172,15 @@ async function parseV3Input<S extends TV3Schemas | undefined, TProps>(
|
||||
let bodyData: unknown;
|
||||
|
||||
try {
|
||||
bodyData = await req.json();
|
||||
} catch {
|
||||
bodyData = await parseJsonBodyWithLimit(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
ok: false,
|
||||
response: problemPayloadTooLarge(requestId, error.message, instance),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
response: problemBadRequest(requestId, "Invalid request body", {
|
||||
|
||||
@@ -71,6 +71,17 @@ export function problemBadRequest(
|
||||
});
|
||||
}
|
||||
|
||||
export function problemPayloadTooLarge(
|
||||
requestId: string,
|
||||
detail: string = "Payload Too Large",
|
||||
instance?: string
|
||||
): Response {
|
||||
return problemResponse(413, "Payload Too Large", detail, requestId, {
|
||||
code: "payload_too_large",
|
||||
instance,
|
||||
});
|
||||
}
|
||||
|
||||
export function problemUnauthorized(
|
||||
requestId: string,
|
||||
detail: string = "Not authenticated",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { parseAndValidateJsonBody } from "./parse-and-validate-json-body";
|
||||
import { DEFAULT_REQUEST_BODY_LIMIT_BYTES } from "./request-body";
|
||||
|
||||
describe("parseAndValidateJsonBody", () => {
|
||||
test("returns a malformed JSON response when request parsing fails", async () => {
|
||||
@@ -39,6 +40,40 @@ describe("parseAndValidateJsonBody", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("returns a payload too large response when the request body exceeds the body limit", async () => {
|
||||
const request = new Request("http://localhost/api/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Length": String(DEFAULT_REQUEST_BODY_LIMIT_BYTES + 1),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
const result = await parseAndValidateJsonBody({
|
||||
request,
|
||||
schema: z.object({
|
||||
finished: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
expect("response" in result).toBe(true);
|
||||
|
||||
if (!("response" in result)) {
|
||||
throw new Error("Expected a response result");
|
||||
}
|
||||
|
||||
expect(result.issue).toBe("payload_too_large");
|
||||
expect(result.response.status).toBe(413);
|
||||
await expect(result.response.json()).resolves.toEqual({
|
||||
code: "payload_too_large",
|
||||
message: "Payload Too Large",
|
||||
details: {
|
||||
error: `Request body must not exceed ${DEFAULT_REQUEST_BODY_LIMIT_BYTES} bytes`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("returns a validation response when the parsed JSON does not match the schema", async () => {
|
||||
const request = new Request("http://localhost/api/test", {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
|
||||
type TJsonBodyValidationIssue = "invalid_json" | "invalid_body";
|
||||
type TJsonBodyValidationIssue = "invalid_json" | "invalid_body" | "payload_too_large";
|
||||
|
||||
type TJsonBodyValidationError = {
|
||||
details: Record<string, string> | { error: string };
|
||||
@@ -44,10 +45,18 @@ export const parseAndValidateJsonBody = async <TSchema extends z.ZodTypeAny>({
|
||||
let jsonInput: unknown;
|
||||
|
||||
try {
|
||||
jsonInput = await request.json();
|
||||
jsonInput = await parseJsonBodyWithLimit(request);
|
||||
} catch (error) {
|
||||
const details = { error: getErrorMessage(error) };
|
||||
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
details,
|
||||
issue: "payload_too_large",
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", details, true),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
details,
|
||||
issue: "invalid_json",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
DEFAULT_REQUEST_BODY_LIMIT_BYTES,
|
||||
RequestBodyTooLargeError,
|
||||
parseJsonBodyWithLimit,
|
||||
readRequestBodyWithLimit,
|
||||
} from "./request-body";
|
||||
|
||||
const createStreamingRequest = (chunks: string[]): Request =>
|
||||
new Request("http://localhost/api/test", {
|
||||
method: "POST",
|
||||
body: new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
for (const chunk of chunks) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
duplex: "half",
|
||||
} as RequestInit & { duplex: "half" });
|
||||
|
||||
describe("request body parsing", () => {
|
||||
test("rejects a request when content-length exceeds the body limit", async () => {
|
||||
const request = new Request("http://localhost/api/test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Length": String(DEFAULT_REQUEST_BODY_LIMIT_BYTES + 1),
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
await expect(readRequestBodyWithLimit(request)).rejects.toMatchObject({
|
||||
actualBytes: DEFAULT_REQUEST_BODY_LIMIT_BYTES + 1,
|
||||
limitBytes: DEFAULT_REQUEST_BODY_LIMIT_BYTES,
|
||||
name: "RequestBodyTooLargeError",
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects a streamed request when the actual body exceeds the body limit", async () => {
|
||||
const request = createStreamingRequest(["a".repeat(DEFAULT_REQUEST_BODY_LIMIT_BYTES), "b"]);
|
||||
|
||||
await expect(readRequestBodyWithLimit(request)).rejects.toBeInstanceOf(RequestBodyTooLargeError);
|
||||
});
|
||||
|
||||
test("allows a body exactly at the body limit", async () => {
|
||||
const rawBody = "a".repeat(DEFAULT_REQUEST_BODY_LIMIT_BYTES);
|
||||
const request = new Request("http://localhost/api/test", {
|
||||
method: "POST",
|
||||
body: rawBody,
|
||||
});
|
||||
|
||||
const body = await readRequestBodyWithLimit(request);
|
||||
|
||||
expect(body).toHaveLength(DEFAULT_REQUEST_BODY_LIMIT_BYTES);
|
||||
expect(body).toBe(rawBody);
|
||||
});
|
||||
|
||||
test("preserves JSON parse errors for malformed bodies under the body limit", async () => {
|
||||
const request = new Request("http://localhost/api/test", {
|
||||
method: "POST",
|
||||
body: "{invalid-json",
|
||||
});
|
||||
|
||||
await expect(parseJsonBodyWithLimit(request)).rejects.toBeInstanceOf(SyntaxError);
|
||||
});
|
||||
|
||||
test("returns an empty string for requests without a body", async () => {
|
||||
const request = new Request("http://localhost/api/test", {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
await expect(readRequestBodyWithLimit(request)).resolves.toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
export const DEFAULT_REQUEST_BODY_LIMIT_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
export class RequestBodyTooLargeError extends Error {
|
||||
readonly actualBytes: number | null;
|
||||
readonly limitBytes: number;
|
||||
|
||||
constructor(limitBytes: number, actualBytes: number | null = null) {
|
||||
super(`Request body must not exceed ${limitBytes} bytes`);
|
||||
this.name = "RequestBodyTooLargeError";
|
||||
this.limitBytes = limitBytes;
|
||||
this.actualBytes = actualBytes;
|
||||
}
|
||||
}
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
const getContentLength = (headers: Headers): number | null => {
|
||||
const contentLength = headers.get("content-length");
|
||||
if (!contentLength) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedContentLength = Number(contentLength);
|
||||
if (!Number.isSafeInteger(parsedContentLength) || parsedContentLength < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsedContentLength;
|
||||
};
|
||||
|
||||
const assertBodySize = (actualBytes: number, limitBytes: number): void => {
|
||||
if (actualBytes > limitBytes) {
|
||||
throw new RequestBodyTooLargeError(limitBytes, actualBytes);
|
||||
}
|
||||
};
|
||||
|
||||
export const readRequestBodyWithLimit = async (
|
||||
request: Request,
|
||||
limitBytes: number = DEFAULT_REQUEST_BODY_LIMIT_BYTES
|
||||
): Promise<string> => {
|
||||
const contentLength = getContentLength(request.headers);
|
||||
if (contentLength !== null) {
|
||||
assertBodySize(contentLength, limitBytes);
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reader = request.body.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let receivedBytes = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
receivedBytes += value.byteLength;
|
||||
if (receivedBytes > limitBytes) {
|
||||
await reader.cancel().catch(() => undefined);
|
||||
throw new RequestBodyTooLargeError(limitBytes, receivedBytes);
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
if (chunks.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (chunks.length === 1) {
|
||||
return textDecoder.decode(chunks[0]);
|
||||
}
|
||||
|
||||
const body = new Uint8Array(receivedBytes);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
body.set(chunk, offset);
|
||||
offset += chunk.byteLength;
|
||||
}
|
||||
|
||||
return textDecoder.decode(body);
|
||||
};
|
||||
|
||||
export const parseJsonBodyWithLimit = async <TJson = unknown>(
|
||||
request: Request,
|
||||
limitBytes: number = DEFAULT_REQUEST_BODY_LIMIT_BYTES
|
||||
): Promise<TJson> => JSON.parse(await readRequestBodyWithLimit(request, limitBytes)) as TJson;
|
||||
@@ -17,7 +17,8 @@ interface ApiErrorResponse {
|
||||
| "not_authenticated"
|
||||
| "forbidden"
|
||||
| "too_many_requests"
|
||||
| "conflict";
|
||||
| "conflict"
|
||||
| "payload_too_large";
|
||||
message: string;
|
||||
details: {
|
||||
[key: string]: string | string[] | number | number[] | boolean | boolean[];
|
||||
@@ -80,6 +81,30 @@ const badRequestResponse = (
|
||||
);
|
||||
};
|
||||
|
||||
const payloadTooLargeResponse = (
|
||||
message: string = "Payload Too Large",
|
||||
details: ApiErrorResponse["details"] = {},
|
||||
cors: boolean = false,
|
||||
cache: string = "private, no-store"
|
||||
) => {
|
||||
const headers = {
|
||||
...(cors && corsHeaders),
|
||||
"Cache-Control": cache,
|
||||
};
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
code: "payload_too_large",
|
||||
message,
|
||||
details,
|
||||
} as ApiErrorResponse,
|
||||
{
|
||||
status: 413,
|
||||
headers,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const methodNotAllowedResponse = (
|
||||
res: CustomNextApiResponse,
|
||||
allowedMethods: string[],
|
||||
@@ -294,6 +319,7 @@ export const responses = {
|
||||
unauthorizedResponse,
|
||||
notFoundResponse,
|
||||
successResponse,
|
||||
payloadTooLargeResponse,
|
||||
tooManyRequestsResponse,
|
||||
forbiddenResponse,
|
||||
conflictResponse,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ZodRawShape, z } from "zod";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { TApiAuditLog } from "@/app/lib/api/with-api-logging";
|
||||
import { formatZodError, handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
@@ -73,10 +74,22 @@ export const apiWrapper = async <S extends ExtendedSchemas>({
|
||||
let parsedInput: ParsedSchemas<S> = {} as ParsedSchemas<S>;
|
||||
|
||||
if (schemas?.body) {
|
||||
let bodyData;
|
||||
let bodyData: Record<string, unknown>;
|
||||
try {
|
||||
bodyData = await request.json();
|
||||
bodyData = await parseJsonBodyWithLimit<Record<string, unknown>>(request);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return handleApiError(request, {
|
||||
type: "payload_too_large",
|
||||
details: [
|
||||
{
|
||||
field: "body",
|
||||
issue: error.message,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
logger.error({ error, url: request.url }, "Error parsing JSON input");
|
||||
return handleApiError(request, {
|
||||
type: "bad_request",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { err, ok } from "@formbricks/types/error-handlers";
|
||||
import { DEFAULT_REQUEST_BODY_LIMIT_BYTES } from "@/app/lib/api/request-body";
|
||||
import { apiWrapper } from "@/modules/api/v2/auth/api-wrapper";
|
||||
import { authenticateRequest } from "@/modules/api/v2/auth/authenticate-request";
|
||||
import { handleApiError } from "@/modules/api/v2/lib/utils";
|
||||
@@ -164,6 +165,42 @@ describe("apiWrapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle oversized JSON input in request body", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
headers: {
|
||||
"Content-Length": String(DEFAULT_REQUEST_BODY_LIMIT_BYTES + 1),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(authenticateRequest).mockResolvedValue(ok(mockAuthentication));
|
||||
vi.mocked(handleApiError).mockResolvedValue(new Response("error", { status: 413 }));
|
||||
|
||||
const bodySchema = z.object({ key: z.string() });
|
||||
const handler = vi.fn();
|
||||
|
||||
const response = await apiWrapper({
|
||||
request,
|
||||
schemas: { body: bodySchema },
|
||||
rateLimit: false,
|
||||
handler,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(413);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
expect(handleApiError).toHaveBeenCalledWith(request, {
|
||||
type: "payload_too_large",
|
||||
details: [
|
||||
{
|
||||
field: "body",
|
||||
issue: `Request body must not exceed ${DEFAULT_REQUEST_BODY_LIMIT_BYTES} bytes`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle empty body when body schema is provided", async () => {
|
||||
const request = new Request("http://localhost", {
|
||||
method: "POST",
|
||||
|
||||
@@ -148,6 +148,35 @@ const conflictResponse = ({
|
||||
);
|
||||
};
|
||||
|
||||
const payloadTooLargeResponse = ({
|
||||
details = [],
|
||||
cors = false,
|
||||
cache = "private, no-store",
|
||||
}: {
|
||||
details?: ApiErrorDetails;
|
||||
cors?: boolean;
|
||||
cache?: string;
|
||||
} = {}) => {
|
||||
const headers = {
|
||||
...(cors && corsHeaders),
|
||||
"Cache-Control": cache,
|
||||
};
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
error: {
|
||||
code: 413,
|
||||
message: "Payload Too Large",
|
||||
details,
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 413,
|
||||
headers,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const unprocessableEntityResponse = ({
|
||||
details = [],
|
||||
cors = false,
|
||||
@@ -351,6 +380,7 @@ export const responses = {
|
||||
forbiddenResponse,
|
||||
notFoundResponse,
|
||||
conflictResponse,
|
||||
payloadTooLargeResponse,
|
||||
unprocessableEntityResponse,
|
||||
tooManyRequestsResponse,
|
||||
internalServerErrorResponse,
|
||||
|
||||
@@ -28,6 +28,8 @@ export const handleApiError = (
|
||||
return responses.notFoundResponse({ details: err.details });
|
||||
case "conflict":
|
||||
return responses.conflictResponse({ details: err.details });
|
||||
case "payload_too_large":
|
||||
return responses.payloadTooLargeResponse({ details: err.details });
|
||||
case "unprocessable_entity":
|
||||
return responses.unprocessableEntityResponse({ details: err.details });
|
||||
case "too_many_requests":
|
||||
|
||||
@@ -10,7 +10,13 @@ export type ApiErrorDetails = {
|
||||
|
||||
export type ApiErrorResponseV2 =
|
||||
| {
|
||||
type: "unauthorized" | "forbidden" | "conflict" | "too_many_requests" | "internal_server_error";
|
||||
type:
|
||||
| "unauthorized"
|
||||
| "forbidden"
|
||||
| "conflict"
|
||||
| "payload_too_large"
|
||||
| "too_many_requests"
|
||||
| "internal_server_error";
|
||||
details?: ApiErrorDetails;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { headers } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { RequestBodyTooLargeError, readRequestBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { webhookHandler } from "@/modules/ee/billing/api/lib/stripe-webhook";
|
||||
|
||||
export const POST = async (request: Request) => {
|
||||
try {
|
||||
const body = await request.text();
|
||||
const body = await readRequestBodyWithLimit(request);
|
||||
const requestHeaders = await headers(); // Corrected: headers() is async
|
||||
const signature = requestHeaders.get("stripe-signature");
|
||||
|
||||
@@ -26,6 +27,10 @@ export const POST = async (request: Request) => {
|
||||
|
||||
return NextResponse.json(result.message || { received: true }, { status: 200 });
|
||||
} catch (error: any) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return NextResponse.json({ message: "Payload Too Large" }, { status: 413 });
|
||||
}
|
||||
|
||||
logger.error(error, `Unhandled error in Stripe webhook POST handler: ${error.message}`);
|
||||
return NextResponse.json({ message: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ZId } from "@formbricks/types/common";
|
||||
import { TContactAttributesInput } from "@formbricks/types/contact-attribute";
|
||||
import { ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
|
||||
import { TJsPersonState } from "@formbricks/types/js";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
@@ -27,6 +28,11 @@ const handleError = (err: unknown, url: string): { response: Response; error?: u
|
||||
};
|
||||
};
|
||||
|
||||
type TContactUserRequestBody = Record<string, unknown> & {
|
||||
attributes?: Record<string, unknown>;
|
||||
userId?: unknown;
|
||||
};
|
||||
|
||||
export const OPTIONS = async (): Promise<Response> => {
|
||||
return responses.successResponse(
|
||||
{},
|
||||
@@ -76,7 +82,18 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
const { workspaceId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
let jsonInput: TContactUserRequestBody;
|
||||
try {
|
||||
jsonInput = await parseJsonBodyWithLimit<TContactUserRequestBody>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }, true),
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Basic input validation without Zod overhead
|
||||
if (
|
||||
@@ -91,8 +108,13 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
// Simple email validation if present (avoid Zod)
|
||||
if (jsonInput.attributes?.email) {
|
||||
const email = jsonInput.attributes.email;
|
||||
const attributes =
|
||||
typeof jsonInput.attributes === "object" && jsonInput.attributes !== null
|
||||
? jsonInput.attributes
|
||||
: undefined;
|
||||
|
||||
if (attributes?.email) {
|
||||
const email = attributes.email;
|
||||
if (typeof email !== "string" || !email.includes("@") || email.length < 3) {
|
||||
return {
|
||||
response: responses.badRequestResponse("Invalid email format", undefined, true),
|
||||
@@ -100,7 +122,7 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
}
|
||||
|
||||
const { userId, attributes } = jsonInput;
|
||||
const userId = jsonInput.userId;
|
||||
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
|
||||
+8
-1
@@ -1,5 +1,6 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { handleErrorResponse } from "@/app/api/v1/auth";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { TApiKeyAuthentication, THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -149,8 +150,14 @@ export const PUT = withV1ApiWrapper({
|
||||
|
||||
let contactAttributeKeyUpdate;
|
||||
try {
|
||||
contactAttributeKeyUpdate = await req.json();
|
||||
contactAttributeKeyUpdate = await parseJsonBodyWithLimit(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON input");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { resolveBodyIds } from "@/app/api/v1/management/lib/workspace-resolver";
|
||||
import { RequestBodyTooLargeError, parseJsonBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
@@ -63,8 +64,14 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
let contactAttributeKeyInput;
|
||||
try {
|
||||
contactAttributeKeyInput = await req.json();
|
||||
contactAttributeKeyInput = await parseJsonBodyWithLimit<Record<string, unknown>>(req);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return {
|
||||
response: responses.payloadTooLargeResponse("Payload Too Large", { error: error.message }),
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ error, url: req.url }, "Error parsing JSON input");
|
||||
return {
|
||||
response: responses.badRequestResponse("Malformed JSON input, please check your request body"),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { logger } from "@formbricks/logger";
|
||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { RequestBodyTooLargeError, readRequestBodyWithLimit } from "@/app/lib/api/request-body";
|
||||
import { verifyFeedbackRecordsGatewayToken } from "@/lib/jwt";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { getBearerTokenFromHeaders } from "@/modules/api/lib/api-key-auth";
|
||||
@@ -147,17 +148,35 @@ const parseTenantId = (tenantId: string | null): string | null => {
|
||||
return ZId.safeParse(tenantId).success ? tenantId : null;
|
||||
};
|
||||
|
||||
const parseJsonBody = async (request: NextRequest): Promise<Record<string, unknown> | null> => {
|
||||
const parseJsonBody = async (
|
||||
request: NextRequest
|
||||
): Promise<
|
||||
| {
|
||||
ok: true;
|
||||
body: Record<string, unknown> | null;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
response: Response;
|
||||
}
|
||||
> => {
|
||||
try {
|
||||
const rawBody = await request.text();
|
||||
const rawBody = await readRequestBodyWithLimit(request);
|
||||
if (!rawBody.trim()) {
|
||||
return null;
|
||||
return { ok: true, body: null };
|
||||
}
|
||||
|
||||
const parsedBody = JSON.parse(rawBody);
|
||||
return parsedBody && typeof parsedBody === "object" ? (parsedBody as Record<string, unknown>) : null;
|
||||
} catch {
|
||||
return null;
|
||||
return {
|
||||
ok: true,
|
||||
body: parsedBody && typeof parsedBody === "object" ? (parsedBody as Record<string, unknown>) : null,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof RequestBodyTooLargeError) {
|
||||
return { ok: false, response: buildGatewayStatusResponse(413, "Payload Too Large") };
|
||||
}
|
||||
|
||||
return { ok: true, body: null };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,7 +227,12 @@ const resolveTenantId = async (
|
||||
}
|
||||
|
||||
if (route.tenantSource === "body") {
|
||||
const body = await parseJsonBody(request);
|
||||
const parseResult = await parseJsonBody(request);
|
||||
if (!parseResult.ok) {
|
||||
return { errorResponse: parseResult.response };
|
||||
}
|
||||
|
||||
const body = parseResult.body;
|
||||
const tenantId = parseTenantId(typeof body?.tenant_id === "string" ? body.tenant_id : null);
|
||||
if (!tenantId) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user