vercel / vercel/storage

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

Open Beginner friendly
#1,086 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
595
Forks
101
Avg merge
2h 24m
Merged PRs (30d)
1

Description

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.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the @vercel/blob put() entry point and trace how abortSignal is handled before and during the request. Verify the behavior with an already-aborted signal and preserve the existing mid-flight abort behavior; done means the pre-aborted call rejects immediately with an AbortError. The default-timeout and error-shape suggestions may need separate scope decisions.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.