refactor: improve error handling and validation for PDF field metadata parsing

This commit is contained in:
Catalin Pit
2026-06-18 14:03:21 +03:00
parent 402809c809
commit 5f7f6698fd
2 changed files with 69 additions and 28 deletions
@@ -1,4 +1,6 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { type TFieldAndMeta, ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { logger } from '@documenso/lib/utils/logger';
import { PDF, rgb } from '@libpdf/core';
import type { FieldType, Recipient } from '@prisma/client';
@@ -114,14 +116,49 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
const recipient = recipientOrMeta;
const rawFieldMeta = parseRawFieldMetaFromPlaceholder(fieldMetaData);
/*
Parse and validate the field metadata. A malformed selection placeholder
(e.g. an unknown validation rule or a default value that doesn't match an
option) is skipped like an invalid field type rather than aborting the whole
upload, which may contain other valid placeholders and files.
*/
let fieldAndMeta: TFieldAndMeta;
const parsedFieldMeta = parseFieldMetaFromPlaceholder(rawFieldMeta, fieldType);
try {
const rawFieldMeta = parseRawFieldMetaFromPlaceholder(fieldMetaData);
const parsedFieldMeta = parseFieldMetaFromPlaceholder(rawFieldMeta, fieldType);
const fieldAndMeta: TFieldAndMeta = ZEnvelopeFieldAndMetaSchema.parse({
type: fieldType,
fieldMeta: parsedFieldMeta,
});
const parsedFieldAndMeta = ZEnvelopeFieldAndMetaSchema.safeParse({
type: fieldType,
fieldMeta: parsedFieldMeta,
});
/*
Surface schema failures as INVALID_BODY (400) instead of letting the raw
ZodError bubble up to the caller as an INTERNAL_SERVER_ERROR (500).
*/
if (!parsedFieldAndMeta.success) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid field metadata for placeholder "${placeholder}": ${parsedFieldAndMeta.error.message}`,
});
}
fieldAndMeta = parsedFieldAndMeta.data;
} catch (error) {
const appError = AppError.parseError(error);
logger.warn(
{
placeholder,
page: page.index + 1,
code: appError.code,
message: appError.message,
},
'Skipping placeholder with invalid field metadata',
);
continue;
}
/*
LibPDF returns bbox in points with bottom-left origin.
+26 -22
View File
@@ -198,12 +198,6 @@ const getDefaultFieldMetaValue = (rawFieldMeta: Record<string, string>) => {
return defaultValue ? normalizePlaceholderSelectionValue(defaultValue) : undefined;
};
const getCheckedFieldMetaValues = (rawFieldMeta: Record<string, string>) => {
const checkedValue = rawFieldMeta.checked;
return checkedValue ? parsePlaceholderOptions(checkedValue) : [];
};
const parseCheckboxValidationRule = (value: string): string => {
const validationRule = CHECKBOX_VALIDATION_RULE_BY_ALIAS[value];
@@ -268,7 +262,7 @@ const applyRadioFieldOptions = (parsedFieldMeta: Record<string, unknown>, rawFie
const applyCheckboxFieldOptions = (parsedFieldMeta: Record<string, unknown>, rawFieldMeta: Record<string, string>) => {
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.CHECKBOX);
const checkedValues = getCheckedFieldMetaValues(rawFieldMeta);
const checkedValues = rawFieldMeta.checked ? parsePlaceholderOptions(rawFieldMeta.checked) : [];
if (!options && checkedValues.length > 0) {
throw new AppError(AppErrorCode.INVALID_BODY, {
@@ -328,21 +322,37 @@ const applyDropdownFieldOptions = (parsedFieldMeta: Record<string, unknown>, raw
}
};
/*
Generic field metadata properties are simple properties consisting of a key and a value.
E.g. 'required=true', 'fontSize=12', 'textAlign=left'
They don't require special handling.
Special field metadata properties are complex properties consisting of a key and a value with multiple parts.
E.g. 'options=Card/Check|Bank Transfer', 'checked=Card|Check', 'selected=Bank Transfer'
They require special handling.
*/
const shouldSkipGenericFieldMetaParsing = (property: string, fieldType: FieldType): boolean => {
if (property === 'options' || property === 'default' || property === 'selected') {
return true;
}
if (fieldType !== FieldType.CHECKBOX && fieldType !== FieldType.RADIO && fieldType !== FieldType.DROPDOWN) {
const isSelectionField =
fieldType === FieldType.CHECKBOX || fieldType === FieldType.RADIO || fieldType === FieldType.DROPDOWN;
if (!isSelectionField) {
return false;
}
return (
if (
property === 'label' ||
property === 'placeholder' ||
property === 'defaultValue' ||
(property === 'checked' && fieldType === FieldType.CHECKBOX)
);
(fieldType === FieldType.CHECKBOX && property === 'checked')
) {
return true;
}
return false;
};
/*
@@ -404,17 +414,11 @@ export const parseFieldMetaFromPlaceholder = (
}
}
if (fieldType === FieldType.RADIO) {
applyRadioFieldOptions(parsedFieldMeta, rawFieldMeta);
}
if (fieldType === FieldType.CHECKBOX) {
applyCheckboxFieldOptions(parsedFieldMeta, rawFieldMeta);
}
if (fieldType === FieldType.DROPDOWN) {
applyDropdownFieldOptions(parsedFieldMeta, rawFieldMeta);
}
match(fieldType)
.with(FieldType.RADIO, () => applyRadioFieldOptions(parsedFieldMeta, rawFieldMeta))
.with(FieldType.CHECKBOX, () => applyCheckboxFieldOptions(parsedFieldMeta, rawFieldMeta))
.with(FieldType.DROPDOWN, () => applyDropdownFieldOptions(parsedFieldMeta, rawFieldMeta))
.otherwise(() => undefined);
return parsedFieldMeta;
};