bug: consume request body before sending response by cstamas · Pull Request #2066 · eclipse-openvsx/openvsx · GitHub
Skip to content

bug: consume request body before sending response - #2066

Merged
netomi merged 6 commits into
eclipse-openvsx:mainfrom
cstamas:consume-payload-before-respond
Aug 18, 2026
Merged

netomi merged 6 commits into
eclipse-openvsx:mainfrom
cstamas:consume-payload-before-respond

Conversation

@cstamas

@cstamas cstamas commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

As otherwise LB/proxies may translate this as server error and produce 50x errors downstream.

Changes:

  • in case of inactive/expired token return 401 instead of 400
  • in case of attempt to publish with expired token, consume (into /dev/null) the payload BEFORE sending response, to cleanly follow HTTP request/response flow

Fixes EclipseFdn/open-vsx.org#12463

Note: Changed code is/will conflict with trusted publishing, where even the "verify" use case has been totally separated out. This PR merely copies the applicable bits from trusted publishing AccessTokenService and adds body consumption before throwing error (and modifies error from 400 to 401).

As otherwise MITM proxies may translate this as server error
and produce 50x errors.
@autumnfound

Copy link
Copy Markdown
Contributor

@cstamas
cstamas marked this pull request as ready for review August 14, 2026 12:12
gnugomez
gnugomez previously approved these changes Aug 18, 2026
Comment thread server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves HTTP semantics and proxy/LB compatibility around personal access token (PAT) validation, ensuring invalid/expired tokens yield 401 Unauthorized and that publish requests drain the request body before responding with an error.

Changes:

  • Update PAT handling to treat expired tokens as invalid during useAccessToken.
  • Return 401 Unauthorized (instead of 400 Bad Request) for invalid/expired tokens on namespace creation and publishing.
  • Extend API tests to cover expired-token scenarios for namespace creation and publishing.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java Updates expectations to 401 and adds expired-token test coverage for namespace creation and publish.
server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java Returns 401 for invalid token in createNamespace and drains publish request body before returning an auth error.
server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java Adds expiration enforcement to useAccessToken by invalidating expired tokens.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread server/src/main/java/org/eclipse/openvsx/accesstoken/AccessTokenService.java Outdated
Comment thread server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java
…n too, correct 401/403

Two follow-ups to review feedback on this PR:

- AccessTokenService#useAccessToken is now @transactional(TxType.REQUIRES_NEW).
  Without it, the setActive(false)/setAccessedTimestamp mutations joined the
  caller's transaction and were rolled back whenever createNamespace threw its
  own (expected) ErrorResultException right after - silently discarding the
  fact that an expired token was ever touched. Matches the same REQUIRES_NEW
  idiom already used by ExtensionScanPersistenceService for an identical reason.

- LocalRegistryService#publish now drains the request body before rejecting
  for a missing publisher agreement, not just for an invalid/expired token -
  eclipse.checkPublisherAgreement(...) can throw before content has been
  touched at all, reproducing the same early-response-unread-body problem
  this PR set out to fix.

Also audited 401 vs 403 usage across the token/permission code paths this PR
touches and found the same 'defaults to 400' gap in several more places:

- LocalRegistryService#verifyToken: a third invalid-token call site this PR
  didn't touch, now 401 for consistency with the other two; its own
  'insufficient access rights' check is now 403 (authenticated, not permitted).
- PublishExtensionVersionHandler#checkPublishPermission (shared by both the
  scan and non-scan publish paths): same 'insufficient access rights' fix, 403.
- EclipseService#checkPublisherAgreement (all 4 throw sites): 403, matching
  the sibling checkEclipseToken's existing 403 in the same file.

Updated the RegistryAPITest assertions that hard-coded the old status, and
the OpenAPI docs on verifyToken/publish, which were stale either way (the
/api/-/publish 400 doc still showed 'Invalid access token' as its example
despite this PR already making that case 401).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
netomi and others added 2 commits August 18, 2026 15:04
Addresses Copilot review feedback: the new expiration check used
isBefore(now), treating a token expiring at exactly now as still
valid. The rest of the codebase treats expiry as inclusive -
expireAccessTokens' "expires_timestamp <= ?1" and
findByExpiresTimestampLessThanEqualAndActiveTrueAndNotifiedFalseOrderById
both use <=. Switch to !isAfter(now) to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The manual try/catch-and-drain in publish()'s catch block only covered
the two rejection sites it explicitly wrapped, and didn't help with
the pre-existing gap in ExtensionService#createExtensionFile (an
oversized-package rejection only reads up to maxContentSize + 1 bytes,
leaving anything beyond the cap unread).

Add DrainOnCloseInputStream, a small FilterInputStream that drains any
remaining unread bytes in close(). Wrap the request body with it once,
at the top of publish(), and rely on try-with-resources to close it on
every exit path - success, an early rejection, or a validation failure
deep inside extensions.publishVersion(...). This replaces the ad hoc
drain-on-catch with a structural guarantee that can't be forgotten by
a future validation check, and - confirmed with a test mirroring
createExtensionFile's exact BufferedInputStream/size-capped-view chain
- also fixes the oversized-package gap, without ExtensionService
needing to know about it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

server/src/main/java/org/eclipse/openvsx/util/DrainOnCloseInputStream.java:45

  • DrainOnCloseInputStream is described/used as a best-effort wrapper, but if the underlying stream throws on close(), the IOException will currently propagate (from super.close()) and can fail the request even after successful processing. Swallow IOException from the delegated close as well so draining/closing never changes the outcome of the request.
    public void close() throws IOException {
        try {
            transferTo(OutputStream.nullOutputStream());
        } catch (IOException ignored) {
            // best-effort drain; connection state doesn't matter once we're closing this stream

server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java:811

  • The linked issue (EclipseFdn/open-vsx.org#12463) has acceptance criteria calling for HTTP 403 on expired/invalid keys, but this change returns 401 (UNAUTHORIZED) for an invalid/expired token. Please confirm the intended status code for PAT failures and align either the implementation/tests or the issue requirements.
            var token = tokens.useAccessToken(tokenValue);
            if (token == null || token.getUser() == null) {
                throw new ErrorResultException(ACCESS_TOKEN_ERROR, HttpStatus.UNAUTHORIZED);
            }

Comment thread server/src/main/java/org/eclipse/openvsx/LocalRegistryService.java
…over the size-limit test

Addresses two follow-ups from review:

- DrainOnCloseInputStream capped close()'s drain at maxBytes without
  accounting for bytes a caller already read normally beforehand.
  ExtensionService#createExtensionFile's own oversized-package detection
  already reads up to maxContentSize + 1 bytes before rejecting; without
  tracking cumulative reads, close() would then drain a fresh maxBytes
  on top, costing up to ~2x maxBytes total for exactly the case the
  bound exists to protect against. Now tracks bytesRead across the
  stream's whole lifetime (matching the existing SizeLimitInputStream
  in the same package) and only drains the remaining budget at close.
  Verified the regression test actually catches this: reverted the fix,
  watched it fail with the exact double-counted numbers, restored it.

- Added ExtensionServiceTest#shouldRejectAPackageExceedingTheMaxContentSize:
  no existing test exercised createExtensionFile's size-limit rejection
  itself (the one stub of getMaxContentSize() in this test class was
  incidental, for an unrelated precondition-check test). Verified
  empirically (mirroring the exact BufferedInputStream/ByteStreams.limit
  chain createExtensionFile builds) that reading past the cap through
  DrainOnCloseInputStream does not throw - only close() enforces the
  bound - so wrapping the test's content in DrainOnCloseInputStream
  with the same cap LocalRegistryService#publish uses makes the test
  match the real call path without changing its outcome.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (3)

server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java:1938

  • This test uses LocalDateTime.now(), but token expiration is evaluated against TimeUtil.getCurrentUTC() in AccessTokenService. Using the system-default clock here can make the test timezone-dependent/flaky; use TimeUtil.getCurrentUTC() for consistency.
        token.setExpiresTimestamp(LocalDateTime.now().minusDays(1));

server/src/main/java/org/eclipse/openvsx/util/DrainOnCloseInputStream.java:36

  • The Javadoc claims the total bytes read over the stream’s lifetime is capped at maxBytes, but the implementation only bounds how many additional bytes close() will drain (it doesn’t prevent callers from reading past maxBytes). This can mislead future callers about the guarantee.
 * The <b>total</b> bytes ever read from the underlying stream over this instance's lifetime -
 * whatever a caller reads normally before rejecting, plus whatever close() then drains - is capped
 * at {@code maxBytes}: without a bound, a client rejected for any reason (invalid token, an
 * oversized upload, ...) could force the server to read and discard an arbitrarily large body
 * before responding, trading the original LB/proxy problem for a resource-exhaustion one.

server/src/test/java/org/eclipse/openvsx/RegistryAPITest.java:1711

  • This test uses LocalDateTime.now(), but token expiration is evaluated against TimeUtil.getCurrentUTC() in AccessTokenService. Using the system-default clock here can make the test timezone-dependent/flaky; use TimeUtil.getCurrentUTC() for consistency.

This issue also appears on line 1938 of the same file.

        token.setExpiresTimestamp(LocalDateTime.now().minusDays(1));

- DrainOnCloseInputStream's Javadoc claimed the total bytes read over
  the stream's lifetime is capped at maxBytes, but the class doesn't
  enforce a hard limit on normal reads - only close()'s own drain is
  bounded to whatever remains of the budget. Reworded to say that
  precisely: the total only stays near maxBytes in combination with a
  caller (like ExtensionService#createExtensionFile) that itself stops
  reading once it's seen enough to reject the request.

- testCreateNamespaceExpiredToken/testPublishExpiredToken used
  LocalDateTime.now() to set an expired timestamp, while
  AccessTokenService evaluates expiry against TimeUtil.getCurrentUTC().
  Not an actual flakiness risk in practice (minusDays(1) stays in the
  past under any real-world timezone offset), but a real inconsistency
  with what the code under test actually uses. Switched both to
  TimeUtil.getCurrentUTC().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@netomi
netomi merged commit 4f962b2 into eclipse-openvsx:main Aug 18, 2026
5 checks passed
@cstamas
cstamas deleted the consume-payload-before-respond branch August 18, 2026 18:00
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.

Open VSX Publishing endpoint returning 503 instead of 403 for rotated / expired key

5 participants