Skip to content

fix(pdf-server): form-field save robustness#591

Merged
ochafik merged 7 commits intomainfrom
ochafik/pdf-form-save-per-field-catch
Apr 2, 2026
Merged

fix(pdf-server): form-field save robustness#591
ochafik merged 7 commits intomainfrom
ochafik/pdf-form-save-per-field-catch

Conversation

@ochafik
Copy link
Copy Markdown
Contributor

@ochafik ochafik commented Apr 2, 2026

What

buildAnnotatedPdfBytes form loop:

  • Per-field try/catch so one throwing write doesn't abort the rest
  • Radio groups pdf-lib misclassifies as PDFCheckBox (missing /Ff flag): write /V + per-widget /AS directly via setButtonGroupValue(); gated on widgets.length > 1 so real single-widget checkboxes still use check()/uncheck()
  • PDFOptionList (multiselect listbox) support — select() the comma-split values that exist in getOptions()
  • form.updateFieldAppearances() after the loop so widgets missing an on-state appearance stream get one (Preview/Acrobat otherwise show "unchanged")

Viewer:

  • <select multiple> input listener now joins all selectedOptions instead of target.value (first only)
  • Clearing a radio from the panel writes {value:false} per widget (was hitting pdf.js's inverted string-coercion)

getAnnotatedPdfBytes: only sends fields whose value differs from baseline; cleared fields still get explicit ""/false sentinels.

Tests

  • maxLength text field followed by a normal field → second still saves
  • radio fixture with Radio flag cleared → "1" lands on widget[1], not widget[0]

#577 dropped the per-field try/catch in buildAnnotatedPdfBytes when it
swapped to type-dispatch, so the first field whose pdf-lib write throws
(max-length text, missing /Yes appearance, radio buttonValue mapping to
neither label nor index, ...) bubbled to the outer catch and silently
dropped every subsequent field. Compounded by getAnnotatedPdfBytes
passing every baseline field, so even an untouched problematic field in
the PDF poisoned the whole save.

- Re-wrap each field write in its own try/catch (warn + continue).
- getAnnotatedPdfBytes now only sends fields whose value differs from
  baseline (still includes explicit ''/false sentinels for cleared
  fields so pdf-lib overwrites the original /V).
- Regression test: a maxLength=2 text field fed a long string, followed
  by a normal field; assert the second one still lands.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@591

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@591

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@591

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@591

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@591

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@591

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@591

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@591

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@591

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@591

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@591

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@591

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@591

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@591

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@591

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@591

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@591

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@591

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@591

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@591

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@591

commit: 17bc2c5

ochafik added 6 commits April 2, 2026 04:10
…hosen widget

Some PDFs (e.g. third-party forms like the IRS f1040 family, and the
demo Form.pdf) omit the /Ff Radio flag bit on button fields, so pdf-lib
classifies a multi-widget radio as PDFCheckBox. The viewer stores
pdf.js's buttonValue (the widget's on-state name, e.g. '0'/'1') as a
string. The PDFCheckBox branch did 'if (value) field.check()', which
always sets the FIRST widget's on-state - so any choice saved as the
first option.

When the value is a string on a PDFCheckBox, treat it as a radio
on-value: write /V and per-widget /AS directly via the low-level
acroField (mirroring PDFAcroRadioButton.setValue minus its
first-widget-only onValues guard). Booleans keep check()/uncheck().

Test: build a radio fixture, clear the Radio flag so reload sees
PDFCheckBox, save with '1', assert /V = /1 and second widget /AS = /1.
clearFieldInStorage wrote the same string clearValue to every widget's
storage, hitting pdf.js's inverted string-coercion bug (the one
setFieldInStorage already works around): the wrong widget rendered
checked. For radio, write {value:false} to every widget instead.
… radio

A single-widget checkbox getting a string value (e.g. 'Yes'/'Off' if
something upstream stores the export string instead of boolean) was
being routed through setButtonGroupValue, which no-ops when no widget's
on-state matches the value - leaving the box unchanged. Gate the
radio-path on widgets.length > 1; single-widget falls through to
check()/uncheck() with liberal truthy/'Off' semantics.
PDFs whose checkbox/radio widgets lack an /AP/N/<onValue> stream: check()
sets /V and /AS but Preview/Acrobat can't render a state with no
appearance, so the field looks unchanged. Call form.updateFieldAppearances()
after the write loop so pdf-lib synthesizes defaults for dirty fields.
Language field in Form.pdf is a PDFOptionList; the save loop skipped it.
Parse the viewer's comma-joined string, select() the subset that exists
in getOptions() (pdf-lib throws on unknowns and there's no edit-mode
fallback like Dropdown has).
target.value on a multiselect is only the first option; join
selectedOptions so the save path gets the full set.
@ochafik ochafik changed the title fix(pdf-server): one bad form field no longer aborts saving the rest fix(pdf-server): form-field save robustness Apr 2, 2026
@ochafik ochafik merged commit 3af0191 into main Apr 2, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant