Skip to content

Fix multipart file upload handling in HTTP client#516

Open
BinoyOza-okta wants to merge 3 commits intomasterfrom
OKTA-1131607
Open

Fix multipart file upload handling in HTTP client#516
BinoyOza-okta wants to merge 3 commits intomasterfrom
OKTA-1131607

Conversation

@BinoyOza-okta
Copy link
Copy Markdown
Contributor

@BinoyOza-okta BinoyOza-okta commented Mar 25, 2026

fix: Resolve multipart file upload failure for theme image endpoints

Problem

All theme image upload endpoints (upload_brand_theme_logo, upload_brand_theme_favicon, upload_brand_theme_background_image) fail with HTTP 400 E0000019 Bad Request because file data is silently dropped during request construction.

Root Cause — Two bugs in the request pipeline:

  1. Generated API code discards post_params: The Mustache template produced form = {}, overwriting the serialized file data returned by param_serialize(). The file bytes were correctly encoded into post_params but never passed to create_request().

  2. http_client.py cannot handle the tuple-based file format: Even if post_params were passed, send_request() expected request["form"]["file"] to be a file path string and would crash calling .split("/") on the (filename, bytes, mimetype) tuple format that files_parameters() produces.

Changes Made

Bug 1 Fix — Template + all 115 generated API files:

  • openapi/templates/api.mustache: Changed form = {}form = post_params if post_params else None
  • All okta/api/*_api.py files regenerated from the updated template

Bug 2 Fix — HTTP client rewrite:

  • okta/http_client.py + openapi/templates/okta/http_client.mustache:
    • Added _remove_content_type_header() helper for case-insensitive Content-Type removal (lets aiohttp.FormData set its own multipart/form-data; boundary=... header per RFC 2046)
    • New list-of-tuples path: handles [(field_name, (filename, bytes, mimetype)), ...] from files_parameters()
    • Preserved legacy dict path for backward compatibility (file-path uploads, OAuth form-urlencoded)
    • Fixed header mutation: creates a local copy of headers instead of mutating shared _default_headers
    • Case-insensitive Content-Type lookup for OAuth url-encoded form handling

api_client.py + openapi/templates/api_client.mustache — Enhanced files_parameters():

  • Added magic-byte detection for PNG (8-byte signature), JPEG (2-byte SOI marker covering all variants), and GIF (GIF87a/GIF89a)
  • Optional Pillow fallback via lazy from PIL import Image inside try/except ImportError — Pillow is not a required dependency
  • logger.warning() emitted when file type cannot be detected, with actionable install instructions
  • Unknown types fall back to application/octet-stream (honest mimetype, server validates)
  • Fixed param_serialize() docstring :return: to match the actual 5-element tuple

request_executor.py + openapi/templates/okta/request_executor.mustache:

  • Fixed mutable default arguments: headers: dict = {}headers: dict = None with if headers is None: headers = {} guard
  • Added form parameter to docstring with both dict and list usage documented

Dependencies:

  • Pillow is not added as a mandatory dependency (zero change to requirements.txt, pyproject.toml)
  • Added extras_require={"images": ["pillow >= 9.0.0"]} in setup.py for optional install via pip install okta[images]

Affected Endpoints

  • POST /api/v1/brands/{brandId}/themes/{themeId}/logo
  • POST /api/v1/brands/{brandId}/themes/{themeId}/favicon
  • POST /api/v1/brands/{brandId}/themes/{themeId}/background-image
  • Any other endpoint using multipart/form-data file uploads via the generated API code path

Usage

with open("logo.png", "rb") as f:
    logo_bytes = f.read()

result = await client.upload_brand_theme_logo(
    brand_id=brand_id,
    theme_id=theme_id,
    file=logo_bytes
)

Testing

  • upload_brand_theme_logo — PNG file → HTTP 201, CDN URL returned
  • upload_brand_theme_favicon — PNG file → HTTP 201, CDN URL returned
  • upload_brand_theme_background_image — JPG/PNG files → HTTP 201, CDN URL returned
  • ✅ OAuth token flow (url-encoded form) — no regression
  • ✅ All JSON-body API calls — no regression (form=None skipped cleanly)
  • ✅ SDK imports without Pillow installed — no ImportError

Breaking Changes

None. Fully backward compatible.

- Fix Content-Type header handling for multipart/form-data requests
- Add proper file data handling for binary uploads in aiohttp
- Remove Content-Type header to allow aiohttp to set boundary automatically
- Update form parameter processing to handle both dict and list formats
- Support new tuple-based file upload format: [(field_name, (filename, filedata, mimetype))]

This fixes the multipart upload endpoints for theme images (logo, favicon, background)
where the Content-Type boundary was being overridden, causing "no multipart boundary param" errors.

Fixes: Theme image upload endpoints returning 400 errors
Related files:
- okta/http_client.py
- okta/api/themes_api.py
@BinoyOza-okta BinoyOza-okta self-assigned this Mar 25, 2026
- Make Pillow an optional dependency (lazy import with try/except)
- Remove Pillow from mandatory requirements, setup.py, pyproject.toml
- Add extras_require so users can `pip install okta[images]`
- Add logger.warning() when file type detection falls back
- Fix mutable default args in RequestExecutor.create_request()
- Fix param_serialize() docstring to match actual return signature
- Use case-insensitive Content-Type header lookup for OAuth forms
- Simplify JPEG detection to cover all SOI marker variants
- Restore issue #131 reference comment for json param handling
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