Skip to content

Conversation

@abhishekbiswas772
Copy link

PR Summary: Fix Form Validation Regressions (#13533)

This PR fixes the form validation regressions described in issue #13533. The problem involved how FastAPI incorrectly handles empty strings in optional form fields.

The Problem

When form fields with optional types and None defaults received empty strings (""), FastAPI treated them as empty strings instead of using the default value (None). This affected:

  1. x-www-form-urlencoded forms
  2. multipart/form-data forms

The Fix

Location: fastapi/dependencies/utils.py:897-945 in the _extract_form_body function

The fix addresses two issues:

  1. Line 934-938: Added a check to exclude empty strings from Form fields when adding values. _get_multidict_value already handles them correctly by returning the default value.
  2. Line 942-944: Modified logic for adding extra fields from received_body to only add fields that are not already defined as body_fields. This prevents overwriting correctly processed default values with empty strings from raw form data.

Changes Made

  • Updated fastapi/dependencies/utils.py to properly handle empty strings in form fields
  • Added comprehensive test coverage in tests/test_forms_empty_string_default_13533.py
  • Verified all existing form-related tests (20 tests) continue to pass

Test Results

  • URL-encoded forms with optional string fields and empty values → returns None
  • URL-encoded forms with optional int fields and empty values → returns None (no parsing error)
  • Multipart forms with optional string fields and empty values → returns None
  • All existing form tests continue to pass

Backward Compatibility

The fix ensures backward compatibility with the expected behavior from FastAPI v0.112.4 while maintaining all current functionality.

Fixes fastapi#13533

This fixes regressions in form handling where empty strings in form
fields were incorrectly treated as values instead of using default
values for optional fields.

## Problem
When form fields with Optional types and None defaults received empty
strings (""), FastAPI incorrectly treated them as empty string values
instead of using the default value (None). This affected both:
- x-www-form-urlencoded forms
- multipart/form-data forms

This regression broke existing applications when upgrading from
v0.112.4 to v0.115.x.

## Solution
Modified the _extract_form_body function in fastapi/dependencies/utils.py:

1. Track defined body_field aliases to distinguish between declared
   fields and extra fields
2. Don't add empty strings for Form fields since _get_multidict_value
   already handles them correctly by returning default values
3. Only add extra fields from received_body that are not already
   defined as body_fields to prevent overwriting correctly processed
   default values

## Tests
- Added comprehensive regression tests in
  tests/test_forms_empty_string_default_13533.py
- All existing form-related tests pass (verified 20+ tests)
- Python 3.8+ compatible (uses typing_extensions.Annotated)
@svlandeg
Copy link
Member

Thanks @abhishekbiswas772! In comparison with the other PR #13537 that also addresses #13533 , can you explain how your approach is better / preferred?

@svlandeg
Copy link
Member

Also, I'll put this PR in draft as long as the test suite fails - feel free to mark as "ready for review" when all tests pass.

@svlandeg svlandeg marked this pull request as draft October 22, 2025 11:03
@svlandeg svlandeg added bug Something isn't working waiting labels Oct 22, 2025
@abhishekbiswas772
Copy link
Author

Why This Approach? - PR #14212 for Issue #13533

The Root Cause Discovery

When we traced through the code, we found that _get_multidict_value() (lines 751-772)
already had the correct logic to handle empty strings:

if (
    value is None
    or (
        isinstance(field.field_info, params.Form)
        and isinstance(value, str)
        and value == ""  # ← Empty strings are caught here
    )
):
    if field.required:
        return
    else:
        return deepcopy(field.default)  # ← Returns the default value (None)

So the function was working perfectly — when it saw an empty string in a Form field,
it returned the default value (None).


But Then the Bug Happened

In _extract_form_body(), after calling _get_multidict_value():

for field in body_fields:
    value = _get_multidict_value(field, received_body)  # ← Returns None (correct!)
    # ... processing ...
    if value is not None:
        values[field.alias] = value  # ← None is not added (correct!)

# BUT THEN... this code blindly re-adds everything:
for key, value in received_body.items():
    if key not in values:
        values[key] = value  # ← Re-adds the empty string "" (WRONG!)

The empty string "" gets added back, overwriting the correct behavior!


Our Fix - Three Strategic Changes

1. Track Which Fields Are Declared

body_field_aliases = {field.alias for field in body_fields}

2. Don't Add Empty Strings for Form Fields

if value is not None and not (
    isinstance(field_info, (params.Form, temp_pydantic_v1_params.Form))
    and isinstance(value, str) and value == ""
):
    values[field.alias] = value

3. Only Add Extra Fields That Aren't Already Declared

for key, value in received_body.items():
    if key not in body_field_aliases and key not in values:
        values[key] = value  # Prevents overwriting!

Why I think this approach is right

follows the existing logic_get_multidict_value() already knows how to handle
empty strings, so we let it do its job

Stops the overwriting — We just needed to prevent the "blindly re-add everything"
loop from undoing the correct work

Minimal change — Instead of rewriting logic that already exists, we added a
simple check: "Only add extra fields that weren't already processed"

Complete fix — Addresses both x-www-form-urlencoded and multipart forms


Why Not Other Approaches?

Alternative 1: Remove the re-add loop completely

  • Breaks legitimate use cases where extra fields need to be passed through

Alternative 2: Add empty string handling in the re-add loop

  • Duplicates logic that already exists in _get_multidict_value()
  • More code, more complexity

Alternative 3: Modify _get_multidict_value()

  • It's not broken, why fix it?
  • Used in other places, higher risk

Our approach: Track what we already processed, don't overwrite it

  • Respects existing logic
  • Minimal code change (13 lines)
  • Maintains all existing functionality
  • Fixes both regressions at once

The Core Insight

The bug wasn't in the form processing logic — that was working fine.

The bug was in the cleanup phase where the code tried to add "extra fields"
but didn't distinguish between:

  • Fields we already processed correctly → don't touch these!
  • Actual extra fields → okay to add these

By tracking body_field_aliases, we made that distinction clear — and the bug went away.

@github-actions github-actions bot removed the waiting label Oct 22, 2025
@abhishekbiswas772 abhishekbiswas772 marked this pull request as ready for review October 22, 2025 14:59
@abhishekbiswas772
Copy link
Author

abhishekbiswas772 commented Oct 24, 2025

please review the changes, please let me know if anything is needed from my side

Copy link
Author

@abhishekbiswas772 abhishekbiswas772 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done chnages

@YuriiMotov YuriiMotov changed the title fix ISSUE 13533 fix ISSUE 13533 (Multiple regressions in the handling of forms & form validation) Oct 28, 2025
@MarinPostma
Copy link

the provided explanation as to why this is a preferable solution do not inspire confidence. Both the code and the response are quite obviously vibe coded. The root code discovery code snippet is taken from code added in this PR but says that it's from code that was there before. It changes more code while pretending that it doesn't.

I don't have all the context on the fix anymore, unfortunately it's been over 6 months since I submitted a patch, but this is not a very serious PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants