Resource library

QA How-To

Testing file upload APIs: A Practical Guide (2026)

Testing file upload APIs in 2026: verify multipart requests, size limits, content, security, storage integrity, retries, presigned URLs, and safe cleanup.

25 min read | 2,931 words

TL;DR

Testing file upload APIs requires contract, integrity, security, resilience, and lifecycle checks. Upload deterministic bytes, validate response and storage metadata, retrieve through an independent path, and compare content or digests. Add exact size boundaries, forged types, authorization, retries, presigned URLs, scanning, and cleanup according to risk.

Key Takeaways

  • Assert stored bytes and durable state, not only a successful HTTP response.
  • Vary filename, declared MIME type, inspected content, and structured validity independently.
  • Test per-file, aggregate request, file-count, quota, and exact byte boundaries.
  • Presigned upload tests must cover key scope, method, headers, size, checksum, expiry, and finalization.
  • Retries and duplicate completion require a stable upload session or idempotency contract.
  • Malware scanning, parsing, quarantine, retention, and cleanup are part of the upload lifecycle.
  • Use deterministic synthetic fixtures, independent digests, isolated storage, and safe logs.

Testing file upload APIs means proving much more than a successful POST. A trustworthy strategy verifies multipart parsing, raw bytes, metadata, authorization, size limits, content validation, storage integrity, retries, malware workflow, and cleanup. Start with the upload contract, then assert both the HTTP response and the durable object that downstream consumers will use.

The highest-risk defects often sit between layers. An API can return 201 Created while storing truncated bytes, trusting a forged MIME type, exposing another tenant's object, or publishing a completion event before scanning finishes. Your tests need independent evidence from each relevant boundary.

This practical 2026 guide uses runnable curl and Playwright API examples, a risk-based matrix, and production-oriented oracles for direct and presigned upload designs.

TL;DR

Test area What to vary What to prove
Transport Multipart, raw body, presigned URL Bytes and headers reach the intended endpoint
Boundaries Empty, just below, at, and above limit Documented size policy is enforced consistently
Content Extension, declared MIME type, signature, malformed structure Validation trusts inspected content, not only client metadata
Authorization Tenant, owner, role, expired token No write, read, replace, or finalize across boundaries
Integrity Hash, byte count, download comparison Stored bytes exactly match accepted input
Resilience Timeout, retry, duplicate completion, interrupted transfer No unintended duplicate or corrupt object
Lifecycle Scan, process, retain, delete State transitions and cleanup follow policy

Create deterministic fixture files, calculate their digests locally, upload through the public contract, retrieve through an independent path, and compare bytes. Negative tests should assert status, error schema, absence of durable side effects, and safe logs.

1. Define Testing File Upload APIs by Contract

Begin by identifying the upload pattern. A classic endpoint accepts multipart/form-data with one or more file parts and text fields. A raw upload endpoint accepts bytes in the request body with metadata in headers. A direct-to-storage flow first creates an upload session, returns a short-lived signed URL or form, receives bytes at object storage, then finalizes or processes the upload. Resumable protocols divide the object into chunks and maintain offset or part state.

Write a contract for each stage:

  • Allowed HTTP method, path, authentication, and authorization.
  • Field names, multiplicity, file count, and metadata schema.
  • Maximum request, file, and tenant quota sizes.
  • Allowed content classes and how actual type is detected.
  • Success status, resource identity, and processing state.
  • Error status and stable machine-readable code.
  • Integrity mechanism, such as digest, checksum, entity tag semantics, or later download comparison.
  • Retry, duplicate, expiry, abort, and cleanup behavior.

Do not infer undocumented behavior from one environment. A reverse proxy may reject a large body before application code, producing a different error shape. Object storage may enforce headers that the session service did not mention. Capture those layers in one externally visible contract or explicitly document multiple failure sources.

The central invariant is: accepted input produces one authorized object with the expected bytes and metadata, while rejected input produces no accessible object or downstream side effect. Every test should map to a part of that invariant.

2. Build Deterministic Fixtures and Independent Oracles

Keep a small version-controlled fixture catalog. Include a valid text file, a valid image, a zero-byte file if supported, a UTF-8 filename, a filename with spaces, a structured file with malformed content, and byte sequences crafted for security cases. Generate very large fixtures during the test so the repository stays small.

Record expected length and a cryptographic digest for valid fixtures. In Node.js:

import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';

export async function sha256(path: string): Promise<string> {
  const bytes = await readFile(path);
  return createHash('sha256').update(bytes).digest('hex');
}

The strongest oracle compares downloaded bytes with original bytes. Metadata from the upload response is useful, but it is not independent if it comes from the same parsing path that might be defective. Where direct download is prohibited, query a trusted test-only storage inspector or verify a downstream parse result plus a stored digest.

Separate logical filename from storage key. Tests should not assume the service stores user filenames directly. Good systems generate safe keys and retain a sanitized display name as metadata. Never make assertions depend on a cloud provider's entity tag being a universal MD5 digest because multipart and encrypted storage can use different semantics.

Fixture files can contain sensitive or malicious-looking data. Label them, isolate test buckets, and ensure security monitoring knows the approved test signatures. Never use real customer documents.

3. Send a Correct Multipart Request

First prove the happy-path request outside the application UI. Curl automatically constructs the multipart boundary when -F is used:

curl --fail-with-body \
  -H "Authorization: Bearer $TEST_TOKEN" \
  -F "purpose=import" \
  -F "file=@tests/fixtures/customers.csv;type=text/csv" \
  https://api.example.test/v1/uploads

Do not manually set Content-Type: multipart/form-data without a boundary. The client must generate a boundary and encode each part. A mismatched or missing boundary is a valuable negative case, not a correct baseline.

Playwright's APIRequestContext accepts a multipart object with file-like values. This test creates valid CSV bytes in memory and asserts a typical asynchronous upload resource:

import { test, expect } from '@playwright/test';
import { createHash } from 'node:crypto';

test('uploads a CSV with verified metadata', async ({ request }) => {
  const buffer = Buffer.from('email,name\nada@example.test,Ada\n', 'utf8');
  const digest = createHash('sha256').update(buffer).digest('hex');

  const response = await request.post('/v1/uploads', {
    headers: { Authorization: `Bearer ${process.env.TEST_TOKEN}` },
    multipart: {
      purpose: 'import',
      file: {
        name: 'customers.csv',
        mimeType: 'text/csv',
        buffer,
      },
    },
  });

  expect(response.status()).toBe(201);
  const body = await response.json();
  expect(body).toMatchObject({
    fileName: 'customers.csv',
    size: buffer.length,
    sha256: digest,
  });
  expect(body.id).toEqual(expect.any(String));
});

Adapt response fields to the published API. A runnable test uses a real base URL, valid token, and documented schema. It should never accept any 2xx status or merely check that an ID exists.

4. Test Size, Count, and Encoding Boundaries

For a maximum file size N, test 0, 1, N-1, N, and N+1 bytes where those values are meaningful. Also test total request size because multipart headers and additional fields add bytes. If the endpoint accepts multiple files, vary per-file size, aggregate size, file count, duplicate field names, and a mix of valid and oversized files.

Generate exact-size data without keeping huge binaries:

const exactBytes = Buffer.alloc(1024 * 1024, 0x61);

Send it as the buffer in the prior multipart example. Keep CI fixtures modest and reserve gateway-limit or very large streaming tests for an environment designed to absorb them. A 5 GB test on every pull request wastes time and can destabilize shared systems.

Verify where rejection occurs. A proxy might return 413 Payload Too Large, the API may return a domain error, or a storage service may reject the signed upload. All can be acceptable if documented, observable, and safe. Confirm no partial object is downloadable and no quota remains consumed after cleanup.

Test filenames as Unicode and control-data inputs, not as filesystem paths. Include spaces, multiple dots, composed and decomposed Unicode where relevant, very long names, reserved characters, and attempts such as ../../secret.txt. The service should sanitize display names and generate storage keys. It should not leak server paths in errors.

For text-based formats, test byte-order marks, UTF-8, invalid byte sequences, line-ending differences, and declared charset when the contract uses it. Transport acceptance and business parsing are separate stages and should have separate states.

5. Validate Content Type and Security Controls

Client-supplied filename extensions and Content-Type headers are untrusted. Send a valid PNG labeled text/plain, plain text named photo.png, a polyglot fixture approved by security, and a structurally corrupted file with a valid magic prefix. Expected behavior depends on the threat model, but it must be explicit.

Build a layered matrix:

Filename Declared type Inspected content Expected result
avatar.png image/png Valid PNG Accept and scan
avatar.png text/plain Valid PNG Reject or normalize per contract
avatar.png image/png Executable bytes Reject
report.csv text/csv Malformed CSV Transport may accept, parser must fail safely
../../a.txt text/plain Plain text Sanitize name or reject, never traverse

Malware scanning is usually asynchronous. An accepted upload may enter pending_scan, then become available or quarantined. Test state transitions, access restrictions before scanning, scanner timeout, unavailable scanner, repeated callbacks, quarantine authorization, and deletion. Never upload live malware. Use the scanner vendor's standard harmless test string only in an isolated, approved environment.

Test archive bombs, nested archives, decompression limits, image dimension limits, parser resource exhaustion, and macro policy according to supported formats. Size after decompression may matter more than transfer size. Verify errors do not echo file contents, signed URLs, access tokens, or internal storage locations.

For a broader baseline, API security testing basics covers authorization, injection, secrets, and logging risks that also apply here.

6. Verify Authorization and Tenant Isolation

Upload authorization is only the first check. Verify create, read, list, replace, finalize, abort, and delete independently. A user allowed to upload an avatar may not be allowed to retrieve another user's source file or overwrite a compliance document. Test object identifiers from a second tenant and confirm a safe denial without revealing existence.

For presigned flows, validate that the session service binds the target key, method, expiry, allowed content length, and required headers. A signed PUT should not become permission to write a different key or reuse the URL after expiration. If a content type or checksum is signed, changing it should fail. Avoid assertions that rely on exact clock seconds because distributed clocks and rounding may create small documented tolerances.

Verify that an upload completed with one identity cannot be finalized under another. Race revocation against upload and finalize steps. Test suspended users, deleted tenants, expired sessions, and changed roles. Decide whether in-flight uploads complete or abort, then test the documented policy.

Storage buckets should not be public unless the product explicitly serves public assets. Public delivery often uses a separate transformed object or CDN path, not the original upload. Confirm source objects remain private, metadata does not expose personal information, and download responses use safe content-disposition rules.

Use random resource IDs or opaque keys, but remember that unpredictability is not authorization. Every operation must enforce ownership server-side.

7. Test Direct-to-Storage and Finalization Flows

A common design has three phases: request an upload session, send bytes to a signed storage URL, then call finalize or wait for an event. Test each phase and the state machine between them. The session response should not claim completion before bytes exist and pass required validation.

Using native fetch for the signed PUT keeps provider-specific SDKs out of a contract test:

import { test, expect } from '@playwright/test';

test('uploads bytes through a signed URL and finalizes', async ({ request }) => {
  const bytes = Buffer.from('quarter,total\nQ1,42\n', 'utf8');

  const sessionResponse = await request.post('/v1/upload-sessions', {
    data: { fileName: 'totals.csv', contentType: 'text/csv', size: bytes.length },
    headers: { Authorization: `Bearer ${process.env.TEST_TOKEN}` },
  });
  expect(sessionResponse.ok()).toBeTruthy();
  const session = await sessionResponse.json();

  const storageResponse = await fetch(session.uploadUrl, {
    method: 'PUT',
    headers: { 'content-type': 'text/csv' },
    body: bytes,
  });
  expect(storageResponse.ok).toBe(true);

  const finalizeResponse = await request.post(
    `/v1/upload-sessions/${session.id}/complete`,
    { headers: { Authorization: `Bearer ${process.env.TEST_TOKEN}` } },
  );
  expect(finalizeResponse.status()).toBe(202);
});

The API may require a checksum, part list, or storage token during completion. Use exactly its schema. Test finalize before upload, wrong size, wrong checksum, expired URL, aborted session, duplicate finalize, and bytes replaced between validation and consumption. A completion endpoint should be idempotent or explicitly reject duplicates without duplicating processing.

Object stores can be eventually consistent in some list or event paths even when direct reads are strongly consistent. Assert the documented boundary and use polling with a bounded timeout for asynchronous processing, not a fixed sleep.

8. Exercise Retries, Concurrency, and Interrupted Transfers

Network failures create ambiguous outcomes. The server may have stored the complete object while the client never received the response. Retry the same logical upload according to the contract and verify it does not create unexpected duplicate business records. An idempotency key, upload-session ID, or deterministic part number can identify the logical operation.

Test two clients uploading the same logical filename at once. Does the API create versions, reject a conflict, overwrite atomically, or generate independent resources? Test two finalization calls racing, an abort racing with completion, and a delete racing with a processor. Assert final durable state and downstream jobs, not only response order.

For resumable uploads, vary chunk size, upload chunks out of order if prohibited, repeat a chunk, skip a chunk, send overlapping offsets, and resume after token expiry. Verify the server reports the authoritative offset or completed parts. Corrupt one chunk and prove the final object is unavailable.

Fault injection should target boundaries:

  • Connection closes before any body bytes.
  • Connection closes mid-body.
  • Storage succeeds but application response is lost.
  • Storage event is delayed or delivered more than once.
  • Scanner or parser times out.
  • Database commit succeeds but job publication is delayed.

Use controlled proxies, test hooks, or dependency fakes approved for the environment. Retrying ordinary POST requests blindly can multiply objects. API idempotency testing explains how to verify stable outcomes around commit points.

9. Verify Lifecycle, Cleanup, and Observability

An upload may move through created, uploading, pending_scan, processing, available, rejected, expired, and deleted. Use the states defined by the product, then test allowed transitions and forbidden ones. A rejected object should not later become available because a delayed success event arrived. A deleted upload should not be resurrected by a retried processor.

Cleanup is part of correctness. Abandoned multipart parts, expired sessions, quarantined objects, derivative files, database metadata, and CDN caches all need retention policies. Use a controllable clock or administrative test endpoint to exercise expiry without waiting days. Confirm cleanup is idempotent and does not delete active objects with similar prefixes.

Observe the system with resource-safe dimensions:

  • Upload requests and accepted bytes by outcome.
  • Rejections by stable reason, not raw filename.
  • Time in scan and processing states.
  • Abandoned session and orphan object counts.
  • Integrity mismatches and duplicate completions.
  • Cleanup backlog and failures.

Correlate an external request ID, upload ID, storage key identifier, and processing job without logging secrets or complete personal filenames. Metrics labels must remain low-cardinality. Alerts should distinguish customer validation errors from infrastructure failures.

An end-to-end test is complete only when it can remove its objects or mark them for reliable cleanup. Use a dedicated test tenant and retention policy as a final safety net.

10. Operationalize Testing File Upload APIs

Divide the suite by cost and risk. Pull requests should run small multipart happy paths, exact boundary cases, error-schema checks, authorization cases, and deterministic content validation. Nightly or pre-release runs can add large streaming uploads, interrupted transfers, scanner failures, concurrency, resumable sessions, and cleanup verification.

Keep a contract table that maps each limit and state to a test ID. Run API-level tests below UI tests because they isolate transport and storage defects faster. Keep a small browser layer for file-picker behavior, drag and drop, progress, cancellation, and user messages. Cypress's built-in selectFile() or Playwright's file input APIs can cover that UI layer, while the API suite owns protocol depth.

Review the strategy whenever limits, storage provider, proxy, scanning service, or processing pipeline changes. A provider migration can preserve HTTP responses while changing checksum semantics, event delivery, or consistency behavior. Release criteria should include no leaked objects, no cross-tenant access, and recoverable processing, not only successful upload percentage.

Finally, run destructive security and capacity cases only in isolated environments with explicit quotas. Testing should not become a denial-of-service incident. Use generated data, capped concurrency, cleanup verification, and monitoring during every large test campaign.

Interview Questions and Answers

Q: What is the minimum reliable assertion for a successful file upload?

Assert the documented response, resource identity, expected size and metadata, then verify durable bytes through an independent read or digest. A 200 or 201 alone cannot prove storage integrity or downstream availability.

Q: Why should tests not trust the MIME type header?

The client controls it and can label arbitrary bytes as an allowed type. The service should inspect file signatures and validate structure according to risk. Tests should vary extension, declared type, and actual content independently.

Q: How do you test the maximum upload size?

Generate exact byte buffers at the documented boundary and immediately around it. Test both per-file and total-request limits, including multipart overhead where relevant. Verify rejected uploads leave no accessible partial object or consumed quota.

Q: How do you verify a presigned upload?

Create a session, upload bytes with the required method and signed headers, finalize, and poll the documented processing state. Also change the key, method, content type, checksum, size, and expiry to prove the signature is properly constrained.

Q: What retry problem makes upload testing difficult?

A client can lose the response after the server stores bytes, so it cannot know whether retrying creates a duplicate. Test the documented idempotency key or session behavior and assert one logical resource and one processing workflow.

Q: How should malware scanning be tested safely?

Use the scanner's harmless standard test fixture in an approved isolated environment. Verify pending, available, quarantine, timeout, and repeated-callback behavior. Never introduce live malware into shared systems.

Q: What authorization checks belong in the suite?

Test create, upload, finalize, read, list, replace, abort, and delete across users, roles, and tenants. Opaque identifiers do not replace server-side ownership checks. Signed URLs also need scope, expiry, and reuse tests.

Q: How do you test resumable uploads?

Interrupt at controlled offsets, resume from the server's authoritative state, repeat and skip chunks, alter order, corrupt bytes, and expire the session. The final object must be complete, correctly ordered, and available only after validation.

Common Mistakes

  • Checking only the success status and never comparing stored bytes.
  • Manually setting multipart content type without a generated boundary.
  • Treating filename extension or client MIME type as proof of content.
  • Testing one maximum-size value while ignoring aggregate request size and multipart overhead.
  • Using real customer documents or live malware as fixtures.
  • Assuming an object-store entity tag always equals an MD5 hash.
  • Ignoring upload states after bytes transfer, including scan, parse, quarantine, and cleanup.
  • Retrying non-idempotent create requests without a stable logical operation ID.
  • Leaving large fixtures, abandoned sessions, or orphan objects after CI runs.
  • Logging signed URLs, authorization headers, personal filenames, or file contents.

Conclusion

Testing file upload APIs requires an end-to-end integrity and security mindset. Verify protocol encoding, boundaries, inspected content, authorization, stored bytes, retry behavior, lifecycle transitions, and cleanup. A successful response is only one piece of evidence.

Start with small deterministic fixtures and independent digest checks, then add presigned, concurrent, interrupted, scanning, and retention scenarios according to risk. The result should prove that every accepted file is correct and controlled, and every rejected file leaves the system safe.

Interview Questions and Answers

What is the most important oracle for a file upload API?

The accepted request must produce one authorized object with the expected bytes and metadata. I verify durable storage independently through a download or trusted digest. Response status alone is insufficient.

Why test MIME type and file extension separately?

Both are controlled by the client and can disagree with actual bytes. I vary extension, declared content type, magic signature, and structural validity to prove the service applies its documented content policy.

How would you test the maximum size?

I generate exact byte buffers at the boundary and immediately around it. I cover per-file, aggregate request, file-count, quota, and decompressed limits. Rejections must leave no accessible partial object or consumed quota.

What risks are specific to presigned uploads?

The signed permission may be too broad in key, method, expiry, headers, or size. Completion may race with upload or trust unverified metadata. I test every phase and assert that final availability requires validated bytes.

How do retries affect upload design?

A response can be lost after storage succeeds, leaving an ambiguous outcome. A session ID or idempotency key should reconcile the retry to one logical object. I test duplicate create, upload, and finalize calls around commit points.

How do you test upload authorization?

I test create, write, finalize, read, list, replace, abort, and delete across roles and tenants. I also verify signed URLs expire and cannot write another key. Opaque object IDs are not an authorization control.

How should asynchronous scanning be tested?

I assert the object is restricted while pending, then control scanner success, detection, timeout, unavailability, and duplicate callbacks. State transitions must be monotonic, observable, and safe to retry.

What should be monitored for file uploads?

I monitor accepted bytes, stable rejection reasons, time in scan and processing, integrity mismatches, abandoned sessions, orphan objects, duplicate completion, and cleanup backlog. Logs correlate IDs without exposing filenames, signed URLs, tokens, or content.

Frequently Asked Questions

How do you test a file upload API?

Send a deterministic fixture through the documented encoding, assert the response schema, then verify the stored object, size, digest, metadata, and processing state. Add negative, authorization, retry, and cleanup cases.

How do you test multipart file uploads?

Let the HTTP client generate the multipart boundary, vary file and text parts, and test missing, duplicate, malformed, and unexpected fields. Do not set a boundaryless multipart content type manually.

What file-size boundaries should be tested?

Test zero and one byte where relevant, then one below, exactly at, and one above each limit. Cover per-file size, total request size, file count, tenant quota, and decompressed size when applicable.

How do you verify uploaded file integrity?

Calculate a digest and byte count before upload, then download through an independent path and compare bytes or a trusted stored digest. Do not assume every object-store entity tag is an MD5 hash.

How do you test a presigned upload URL?

Upload with the required method and headers, then finalize and verify processing. Change key, method, content type, size, checksum, and expiry to prove the signature is constrained.

How should malware upload tests be performed?

Use a harmless scanner test fixture only in an approved isolated environment. Verify pending, available, quarantine, timeout, retry, access, and cleanup behavior without using live malware.

How do you test interrupted or resumable uploads?

Close connections at controlled offsets, repeat and skip chunks, alter order, corrupt a part, resume from authoritative server state, and expire the session. The final object must remain unavailable until complete and validated.

Related Guides