#6080·verdaccio

[Bug]: JWT Bearer npm publish PUT always 400s 'request size did not match content length' (Basic/AES fine) — core middleware, no plugins

Author: maxispossibleCreated Jul 28, 2026Updated Sep 4, 2026
Labelsfeat: auth6.x branch (latest)

What happened?

Every npm publish authenticated with a JWT Bearer token (i.e. with security.api.jwt configured) fails with:

400 Bad Request - PUT http://localhost:4873/@acme%2frepro-probe - request size did not match content length

The identical request succeeds with Basic auth, and also succeeds with AES legacy tokens (no security.api.jwt block). Only the JWT middleware path corrupts/loses the request body. This reproduces on the stock Docker image with default local storage — no plugins — so it is core middleware, not a storage backend.

Minimal repro (stock image, ~2 minutes):

config.yaml:

yaml
listen: 0.0.0.0:4873
storage: /verdaccio/storage/data
security:
  api:
    jwt:
      sign:
        expiresIn: 30d
  web:
    sign:
      expiresIn: 7d
auth:
  htpasswd:
    file: /verdaccio/storage/htpasswd
    max_users: 1000
packages:
  '@acme/*':
    access: $authenticated
    publish: acme-ci
    unpublish: acme-ci
  '**':
    access: $all
log: { type: stdout, format: pretty, level: info }
bash
docker run -d --name v -p 4873:4873 \
  -v "$PWD/config.yaml":/verdaccio/conf/config.yaml:ro verdaccio/verdaccio:6.9.0

# create user + mint a JWT via the npm token API
curl -s -XPUT http://localhost:4873/-/user/org.couchdb.user:acme-ci \
  -H 'Content-Type: application/json' -d '{"name":"acme-ci","password":"repro-pass-123"}'
TOKEN=$(curl -s -XPOST http://localhost:4873/-/npm/v1/tokens \
  -u acme-ci:repro-pass-123 -H 'Content-Type: application/json' \
  -d '{"password":"repro-pass-123","readonly":false,"cidr_whitelist":[]}' | jq -r .token)

# any minimal package:
mkdir p && cd p && npm init -y >/dev/null && npm pkg set name=@acme/repro-probe

# FAILS (Bearer JWT):
npm publish --registry http://localhost:4873/ \
  --//localhost:4873/:_authToken="$TOKEN"
# => 400 request size did not match content length

# SUCCEEDS (Basic, same package/route):
npm publish --registry http://localhost:4873/ \
  --//localhost:4873/:_auth="$(printf 'acme-ci:repro-pass-123' | base64)"

A raw fetch PUT with an explicit correct Content-Length shows the same split (Bearer → 400, Basic → normal 409/201), so it is not an npm-client artifact.

Where it seems to live: in @verdaccio/auth's apiJWTmiddleware, the request is req.pause()d and next() resumes it. The JWT verification branch (handleJWTAPIMiddleware → "handle jwt token") calls next() synchronously after getMiddlewareCredentials, while the Basic/AES branches go through the async authenticate() callback. Experimentally deferring the JWT branch's next() with setImmediate changes the failure from the 400 to 500 stream is not readable — consistent with the paused stream's buffered data being lost/consumed differently on the synchronous path before body-parser/raw-body reads it.

Impact: any registry that enables security.api.jwt (required for npm-token-API-minted tokens to authenticate the npm API) cannot publish with those tokens at all; operators are forced back to long-lived Basic credentials for publish lanes.

Reproduced identically on 6.7.4, 6.8.0, and 6.9.0 (digest-pinned stock images), Docker on Linux, npm 10/11, no reverse proxy.

Version

6.x (Stable)

Version details

6.9.0 (also reproduced on 6.7.4 and 6.8.0 — stock verdaccio/verdaccio Docker images)

Output server log info

info <-- 172.17.0.1 requested 'PUT /@acme%2frepro-probe'
http <-- 400, user: acme-ci(172.17.0.1), req: 'PUT /@acme%2frepro-probe', error: request size did not match content length

Node.js Version

In-image Node (verdaccio/verdaccio:6.9.0); client Node 22/24, npm 10/11

Package manager

npm

Operating system

Linux (Docker)

Using reverse proxy

No reverse proxy — direct to the container port.