MockWebServer can send extra HTTP/2 stream resets before consuming queued responses

Open
#9,821 3 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
kotlin
Domain
networking

Research direction

Start by reading Http2SocketHandler.onStream() and dispatchBookkeepingRequest() in MockWebServer.kt, then run HttpOverHttp2Test.recoverFromMultipleRefusedStreamsRequiresNewConnection. Trace when dispatcher.dispatch(request) consumes each queued response relative to sending REFUSED_STREAM. Done means the two scripted refusals are consumed once and the retry receives "abc"; the report says the debugger was needed to reproduce the failure.

Written by the indexing model from the issue text.

Description

bug

Related to #9816

I was investigating HttpOverHttp2Test.recoverFromMultipleRefusedStreamsRequiresNewConnection at 67c4f34e7. The test queues two responses that reset the stream with REFUSED_STREAM, followed by a successful response containing "abc".

In Http2SocketHandler.onStream(), MockWebServer reads dispatcher.peek() and applies onRequestStart before calling dispatchBookkeepingRequest(). For CloseStream, this sends the reset before dispatcher.dispatch(request) consumes the queued response.

Since stream handlers run concurrently, a client can receive the reset and retry while the previous handler has not yet consumed its response. The retry can then observe the same queued response.

I reproduced this with a debugger:

  1. Run recoverFromMultipleRefusedStreamsRequiresNewConnection.
  2. Set a breakpoint on requestQueue.add(request) inside dispatchBookkeepingRequest() in MockWebServer.kt.
  3. Set the breakpoint to suspend only the current thread.
  4. Leave the handler suspended so the client can retry.

The client received two refusals on the original connection, opened another connection, and received a third refusal there. The call then failed with StreamResetException: REFUSED_STREAM, even though only two refusals were queued.

I would expect each scripted refusal to be consumed once, with the third attempt receiving "abc".

The breakpoint deliberately forces a delay between sending the reset and consuming the response. I haven’t reproduced this exact failure without the debugger or established whether it explains the Windows CI failure that prompted the investigation.

Environment: Arch Linux x86_64, Java 21, Gradle 9.8.2.

Dominant language
Kotlin
Stars
47.1k
Forks
9.3k
Avg merge
1d 6h
Merged PRs (30d)
29

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

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.

More from lysine-dev/okhttp

All issues in lysine-dev/okhttp

Similar issues

More Kotlin issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.