Skip to content

@vercel/blob: put() has no default timeout, and ignores an already-aborted abortSignal — a stalled connection hangs forever #1086

Description

@wifelette

Summary

Two related problems, both of which turn a transient network stall into an indefinite hang rather than an error:

  1. put() has no default timeout. If the connection stalls, it never resolves and never rejects.
  2. put() ignores an already-aborted abortSignal. Passing a signal that is aborted before the call hangs instead of throwing immediately.

The first is arguably by design. The second looks like a straightforward bug.

Why this bites harder than a slow request

A hang is not a failure, and callers written to handle failure cannot handle it.

In our case a launchd job uploaded a receipt, the connection stalled, and put() waited forever. The caller had a correct fallback ("on error, carry on without the attachment") — but it never ran, because a promise that never settles never rejects. And because launchd will not start a job whose previous run is still alive, every subsequent run was skipped too. One stalled upload silently wedged the whole pipeline for hours, with no error anywhere.

An AbortError after 30s would have been completely recoverable. An infinite wait was not.

Repro 1 — no default timeout

Any stalled connection reproduces it; there's nothing to catch:

import { put } from '@vercel/blob'

// If the connection stalls, this never resolves and never rejects.
// No default deadline, so there is nothing for a caller to recover from.
await put('receipt.pdf', bytes, { access: 'public', token })

Observed live: put() sat for 50+ minutes across several invocations during a transient stall. The same token against the REST endpoint via curl answered in 0.29s once the stall passed, and put() then uploaded the same 958KB file in 354ms — so the SDK was fine; it simply had no deadline to hit.

Repro 2 — an already-aborted signal is ignored

import { put } from '@vercel/blob'

// Signal is aborted ~immediately, before the request starts.
await put('receipt.pdf', bytes, {
  access: 'public',
  token,
  abortSignal: AbortSignal.timeout(1),
})
// Expected: throws (AbortError) essentially at once.
// Actual:   hangs indefinitely. Killed it at 2 minutes.

With a realistic budget (AbortSignal.timeout(30_000)) the abort does fire mid-flight and throws Vercel Blob: The request was aborted. — so the signal is honoured during a request, just not checked before one starts.

Environment

  • @vercel/blob 2.4.0, also reproduced on 2.6.1
  • Node v25.9.0, macOS (arm64)
  • Plain fetch to the same REST endpoint with the same token: fine throughout

Suggested fixes

  1. Check abortSignal.aborted before starting the request and reject immediately with an AbortError. This is the actual bug, and it's a one-liner.
  2. Ship a default timeout (or document loudly that there isn't one). Anything network-bound with no deadline is a hang waiting to happen, and callers reasonably assume a promise eventually settles.
  3. Consider surfacing a real AbortError (err.name === 'AbortError') rather than only a wrapped Error whose message reads "Vercel Blob: The request was aborted." — right now callers have to string-match the message to tell a timeout from a genuine network failure.

Happy to send a PR for (1) if that's useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions