)]}'
{
  "log": [
    {
      "commit": "4c8e5d568399fbcc2df8f565df9795f3768806c0",
      "tree": "a4063a327af2ee4fbcf36478ca6caae28e084109",
      "parents": [
        "8f36c2085822b943b4fa626d657f065b590d2076"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Oct 23 08:42:09 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 23 08:42:09 2026 +0100"
      },
      "message": "protect 1.5.x and enable CI (#1367)"
    },
    {
      "commit": "8f36c2085822b943b4fa626d657f065b590d2076",
      "tree": "f4c6bd403b3b4d7fe253cd2314c9d4a0bc5a8f76",
      "parents": [
        "27545c3f243d1ff194aa31017ebd3e8147aac402"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Oct 20 21:56:14 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 20 20:56:14 2026 +0100"
      },
      "message": "Update jackson-databind to 3.2.4 (#1362)"
    },
    {
      "commit": "27545c3f243d1ff194aa31017ebd3e8147aac402",
      "tree": "f2b5634aa3fd2683981a2c3ce837a4e4ce35c3d5",
      "parents": [
        "cb936b939f92d5c1bfac476aeb1a13c1dd632d5a"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Mon Oct 19 08:47:52 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 19 08:47:52 2026 +0100"
      },
      "message": "build(deps): Bump actions/setup-java from 6.0.1 to 6.1.0 (#1361)\n\nBumps [actions/setup-java](https://github.com/actions/setup-java) from 6.0.1 to 6.1.0.\n- [Release notes](https://github.com/actions/setup-java/releases)\n- [Commits](https://github.com/actions/setup-java/compare/de7274f081f381c8f8158605e0321c36c376e2e6...b843ebf9b913146b38da2eaa7aeddf76074ae1ad)\n\n---\nupdated-dependencies:\n- dependency-name: actions/setup-java\n  dependency-version: 6.1.0\n  dependency-type: direct:production\n  update-type: version-update:semver-minor\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "cb936b939f92d5c1bfac476aeb1a13c1dd632d5a",
      "tree": "ac66b19f537ab2530bc01b3e303f9a5f4be458aa",
      "parents": [
        "b4fb6cac64c25c78d641b024a1ca4941aa9b659b"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Mon Oct 19 08:47:46 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 19 08:47:46 2026 +0100"
      },
      "message": "build(deps): Bump coursier/cache-action from 8.1.1 to 8.2.0 (#1360)\n\nBumps [coursier/cache-action](https://github.com/coursier/cache-action) from 8.1.1 to 8.2.0.\n- [Release notes](https://github.com/coursier/cache-action/releases)\n- [Commits](https://github.com/coursier/cache-action/compare/95e5b1029b6b86e7bac033ee44a0697d8a527d2d...057d08c2f9a2c4e7a4d33df3aae657b7db88b8cc)\n\n---\nupdated-dependencies:\n- dependency-name: coursier/cache-action\n  dependency-version: 8.2.0\n  dependency-type: direct:production\n  update-type: version-update:semver-minor\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "b4fb6cac64c25c78d641b024a1ca4941aa9b659b",
      "tree": "e38d949b9b49f0ecf126efd41c146b63984cf97f",
      "parents": [
        "6f14a60e216551cb2b125672942dbd5c67b463a3"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Mon Oct 19 08:47:40 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 19 08:47:40 2026 +0100"
      },
      "message": "build(deps): Bump sbt/setup-sbt from 1.5.11 to 1.5.12 (#1359)\n\nBumps [sbt/setup-sbt](https://github.com/sbt/setup-sbt) from 1.5.11 to 1.5.12.\n- [Release notes](https://github.com/sbt/setup-sbt/releases)\n- [Commits](https://github.com/sbt/setup-sbt/compare/6158cb0903b8ceeae04f830055f3155e1b6a5ad7...ecad189dd245444b9ae0a0cd5922fd938b88906f)\n\n---\nupdated-dependencies:\n- dependency-name: sbt/setup-sbt\n  dependency-version: 1.5.12\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "6f14a60e216551cb2b125672942dbd5c67b463a3",
      "tree": "6db5a3fd414ff7a2242dcf262f59a6ccc993fa20",
      "parents": [
        "4265d809af4e044d90bed0d283d50e4a7741ae63"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Mon Oct 19 08:45:04 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 19 08:45:04 2026 +0100"
      },
      "message": "fix: escape control characters in parse-error details and document the raw request target (#1294)\n\n* Escape control characters in parse-error details and document the raw request target\n\nMotivation:\n`UriParser.fail` and `HeaderParser.parseError` build the `detail` of their\n`ErrorInfo` from parboiled2\u0027s `formatErrorLine`, which reproduces the\nfailing input line verbatim under a caret. That input is the raw request\ntarget or header value as the peer sent it, and the shipped\n`error-logging-verbosity \u003d full` writes the detail to the log at warning\nlevel, so a client could put a CR, ESC or NUL into a request target and\nhave it land raw in the log: a forged log line, or a terminal control\nsequence for whoever tails it. The summary was already safe, because\nparboiled2 escapes the offending character there; only the line was not.\n\n#1246 handed the same raw request target to custom `ParsingErrorHandler`s\nthrough `IllegalRequestContext`. Its scaladoc said the value must be\nescaped, but `toString` did not, the `error-handler` comment in\n`reference.conf` still said the handler had no access to the request, and\nno documentation page mentioned the handler at all.\n\nModification:\nAdd `ParseErrorLine.render`, which renders the failing line the way\n`formatErrorLine` does but escapes control characters as `\\t`, `\\r`, `\\n`\nand `\\uXXXX`, keeping the caret aligned under the escaped text. The EOI\nsentinel U+FFFF is deliberately left alone, since `HeaderParser` appends\nit to every value and strips it from the error afterwards. Use it in both\nparsers. Escape the raw request target in `IllegalRequestContext.toString`\nso that logging the context is safe, and say so in the scaladoc; add the\nwarning to the Java accessor. Bring the `error-handler` comment up to\ndate and note at `error-logging-verbosity` what `full` logs. Add a\n\"Requests that fail to parse\" section to the low-level server docs\ndescribing the handler, the context and the escaping requirement.\n\nResult:\nA parse error\u0027s detail carries no raw control characters, so a client can\nno longer inject into the log through a request target or header value.\nThe raw request target is documented as attacker-controlled everywhere a\nuser meets it, and the one rendering the library provides is escaped.\n\nTests:\n- New `ParseErrorLineSpec` covers caret placement, end-of-input, escaping\n  and caret alignment past an escaped character.\n- `UriSpec`, `HeaderSpec` and `RequestParserSpec` each gain a case sending\n  an ESC or NUL and asserting the escaped detail, and the request-parser\n  case asserts the escaped `toString`. All three fail with the change to\n  the parsers and `toString` reverted.\n- sbt \"http-core/testOnly ...ParseErrorLineSpec ...UriSpec ...HeaderSpec\n  ...RequestParserCRLFSpec ...RequestParserLFSpec\" - 197 tests pass.\n- sbt \"http-core/mimaReportBinaryIssues\" - pass.\n- sbt \"docs/paradox\" - pass; the new apidoc links resolve.\n\nReferences:\nRefs #1246, #1245\n\n* Align case arrows in ParseErrorLine.escape per scalafmt\n\n* Expect the escaped NUL in HttpHeaderSpec\u0027s illegal-header-value detail"
    },
    {
      "commit": "4265d809af4e044d90bed0d283d50e4a7741ae63",
      "tree": "b631618fb52a8858193610106bf09e8d105daa7e",
      "parents": [
        "6f845e4abca829f94e18ca68550cee5a3470a237"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Mon Oct 19 08:41:54 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 19 08:41:54 2026 +0100"
      },
      "message": "perf: copy ASCII bytes with the bulk String.getBytes instead of a charAt loop (#1299)\n\nMotivation:\nEnhancedString.getAsciiBytes copied a string into a byte array one\ncharacter at a time via String.charAt, which on JDK 9+ pays a coder\nbranch and a bounds check per character.\n\nModification:\nUse String.getBytes(int, int, byte[], int), the JDK primitive with the\nsame low-8-bits semantics. It is a single System.arraycopy for a\nLatin-1 coded string and one tight loop for a UTF-16 coded one. The\nmethod is deprecated (not for removal), so it is annotated with\n@nowarn(\"cat\u003ddeprecation\"). Add EnhancedStringSpec pinning the\ntruncation, offset and short-array behaviour, and a JMH benchmark.\n\nResult:\nasciiBytes is ~1.4-2x faster and getAsciiBytes ~2.6-8.5x faster on\nJDK 17 (see the PR for the JMH table); rendered bytes are unchanged.\n\nTests:\n- sbt \"http-core/Test/testOnly org.apache.pekko.http.impl.util.EnhancedStringSpec\" (Scala 2.13.18 and 3.3.8)\n- sbt \"http-bench-jmh/Jmh/run -f 2 -wi 5 -i 8 -w 1 -r 2 EnhancedStringBenchmark\"\n- sbt http-core/mimaReportBinaryIssues\n- scalafmt --mode diff-ref\u003dorigin/main; sbt headerCreateAll\n\nReferences:\nNone - follow-up to the recent header parsing hardening work"
    },
    {
      "commit": "6f845e4abca829f94e18ca68550cee5a3470a237",
      "tree": "61a47e5cb8457c107ef4216c830924f84289e31e",
      "parents": [
        "db91db147a495169d64e51c14cf00fefe0a309e1"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Mon Oct 19 08:40:43 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 19 08:40:43 2026 +0100"
      },
      "message": "perf: Huffman decode HPACK string literals into reusable scratch arrays (#1302)\n\nMotivation:\nHuffmanDecoder collected the decoded symbols in a ByteArrayOutputStream,\none synchronized write(int) per symbol, growing it from 32 bytes by\ndoubling, and copied it out with toByteArray; Decoder then copied the\nresult again into a String. The raw literal path allocated and copied\nonce through InputStream.readNBytes and again into the String. This\nruns for every string literal of every inbound header block, which\nbrowsers Huffman code almost entirely.\n\nModification:\nDecoder keeps two scratch arrays, reused across header blocks: the coded\nliteral is read into one with readNBytes(byte[], int, int), and\nHuffmanDecoder.decode(buf, length, out) writes the symbols by index into\nthe other, which the caller sizes from the static bound that the shortest\ncode in the HPACK Huffman table is 5 bits (maxDecodedLength). The String\nis built straight from the scratch array with the ISO-8859-1 charset, the\nsingle copy left in either path. The arrays grow to the longest literal\nseen on the connection, which the max header list size and the dynamic\ntable capacity bound. A MiMa exclude covers the package-private\nHuffmanDecoder signature.\n\nResult:\nNo lock, no stream growth and one copy instead of two or three per\nstring literal: decoding a typical browser request\u0027s header block is\nabout 20-25% faster Huffman coded and 35-40% faster raw on JDK 17 (see\nthe PR for the JMH figures, taken on a loaded laptop).\n\nTests:\n- sbt \"http-core/Test/testOnly org.apache.pekko.http.shaded.com.twitter.hpack.HuffmanDecoderSpec org.apache.pekko.http.impl.engine.http2.hpack.HpackDecoderSpec\" (Scala 2.13.18 and 3.3.8)\n- sbt \"http2-tests/Test/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.RequestParsingSpec\"\n- sbt \"http-bench-jmh/Jmh/run -f 1 -wi 3 -i 5 -w 1 -r 1 HpackDecoderBenchmark\" on main and on this branch\n- sbt http-core/mimaReportBinaryIssues; sbt headerCheckAll; sbt javafmtCheckAll (JDK 17)\n- scalafmt --mode diff-ref\u003dupstream/main\n\nReferences:\nRefs #1301 - the same change for the encoder; Refs #1251 - the InputStream decoding this builds on"
    },
    {
      "commit": "db91db147a495169d64e51c14cf00fefe0a309e1",
      "tree": "199ecfcdcf3bdcf63270656454f47bb87d19b286",
      "parents": [
        "8aa98a6ade0fe4664dce831d383911e49aca4306"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Mon Oct 19 00:07:42 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Oct 18 23:07:42 2026 +0100"
      },
      "message": "Update jackson-databind to 2.22.4 (#1357)"
    },
    {
      "commit": "8aa98a6ade0fe4664dce831d383911e49aca4306",
      "tree": "f8dcf266dc10ad904b5f71ee8ee02ce25be5e85a",
      "parents": [
        "fc6106ea043fd593f8e4fccc16bd068d66268b26"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Fri Oct 16 19:30:26 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 16 18:30:26 2026 +0100"
      },
      "message": "Update classgraph to 5.0.0 (#1355)\n\nMotivation:\nclassgraph 6.0.0 is compiled for Java 25 (class file version 69) and\ncannot run on Java 17. 5.0.0 targets Java 17 but has a reworked API.\n\nModification:\n- Use classgraph 5.0.0 instead of 6.0.0.\n- Migrate test code to the 5.x API: getDirectSuperinterfaces,\n  getAllSuperclasses and getDirectAnnotationInfo.\n- Pass the spec\u0027s ClassLoader via enableClassLoaders, since 5.x scans\n  nothing unless a classpath source is enabled.\n- Pin classgraph to 5.x in .scala-steward.conf.\n\nResult:\nTest-only classgraph dependency is upgraded and still runs on Java 17.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.javadsl.DirectivesConsistencySpec\" (JDK 17): 321 succeeded, same as with 4.8.199\n- sbt \"docs/testOnly docs.ApiMayChangeDocCheckerSpec\" (JDK 17): 2 succeeded, same as with 4.8.199\n- scalafmt: no changes\n\nReferences:\nRefs #1355\n\nCo-authored-by: PJ Fanning \u003cpjfanning@users.noreply.github.com\u003e"
    },
    {
      "commit": "fc6106ea043fd593f8e4fccc16bd068d66268b26",
      "tree": "dc00518ad922de131dbb9bff193ac0f2678c2393",
      "parents": [
        "a5445d8300105831df6a775e81ab6a68170cfb6b"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Wed Oct 14 19:48:14 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 14 19:48:14 2026 +0200"
      },
      "message": "Update classgraph to 4.8.199 (#1353)"
    },
    {
      "commit": "a5445d8300105831df6a775e81ab6a68170cfb6b",
      "tree": "cdffc3ff1b74552dd1329e8ec5c27b3e422d31cc",
      "parents": [
        "04db0095ebbaf9ec9589a0944556823824c99459"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Wed Oct 14 19:47:47 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 14 19:47:47 2026 +0200"
      },
      "message": "Update caffeine to 3.3.1 (#1352)"
    },
    {
      "commit": "04db0095ebbaf9ec9589a0944556823824c99459",
      "tree": "97695d2f16f0b525d318e78c96678f2c37d4c617",
      "parents": [
        "265a6cebae5d0b4409f257ed72723539e1c2da71"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Wed Oct 14 08:37:07 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 14 08:37:07 2026 +0100"
      },
      "message": "release notes for 1.4.2 (#1345)"
    },
    {
      "commit": "265a6cebae5d0b4409f257ed72723539e1c2da71",
      "tree": "693205fd86a3881e087b056a5461840fa546dfb1",
      "parents": [
        "0808b90b2c507708bfe76fed6d55db3c2dee2818"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Oct 13 19:26:32 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 19:26:32 2026 +0100"
      },
      "message": "fix: reject control bytes in the request line and malformed HTTP/2 :scheme and field names (#1341)\n\n* test: cover control characters around the request-line version token\n\nMotivation:\nnetty/netty#16970 was a request-smuggling bug in which control bytes around the HTTP\nversion token were stripped before matching. #1088 covered a NUL byte in the request\ntarget, but nothing covered control bytes next to the version, in the method, or the\nother control characters #1087 lists.\n\nModification:\nAdd RequestParserSpec cases, run in both the CRLF and LF variants, for:\n- NUL, VT, FF and bare CR immediately before and after HTTP/1.1 (505)\n- NUL before and inside the method (501)\n- VT and FF in the request target (400)\nInputs bypass prep so the CR cases keep their exact bytes.\n\nResult:\nA future parser change that trims control bytes around the version token fails these\ntests.\n\nTests:\n- sbt \"http-core/testOnly *RequestParserCRLFSpec *RequestParserLFSpec\": 138 passed\n- With HttpRequestParser temporarily changed to skip NUL/VT/FF/bare CR around the\n  version, \"a control character before or after the HTTP version\" fails\n- sbt http-core/Test/scalafmtCheck: pass\n\nReferences:\nFixes #1087\n\n* fix: reject request-line and HTTP/2 field bytes that re-parse differently\n\nMotivation:\nProbing the request-line parser for #1087 found that a CR or LF ending the request\ntarget was skipped as if it were the SP separator, so `GET /\u003cLF\u003eHTTP/1.1` was accepted\nas one request line where another hop may see a line without a version. The HTTP/2\nequivalents - control bytes in pseudo-header and field values - turned up two more:\n- an invalid \u0027:scheme\u0027 (a control character or whitespace) made Uri.apply throw an\n  IllegalUriException out of the request parsing stage, failing the whole connection\n  instead of the one stream\n- a regular field name containing \u0027:\u0027 was handed to the HTTP/1.1 line parser as\n  `name: value` and split at the colon, so `x:a: v` became a field `x` carrying `a: v`\n\nModification:\n- HttpRequestParser: a CR or LF ending the request target is answered with 505, as a\n  request line without a version already was.\n- Http2HeaderParsing.Scheme: validate the value during decompression, as \u0027:path\u0027 and\n  \u0027:authority\u0027 are, and report a malformed one as a ParsingException. An empty value\n  is still accepted because Pekko HTTP/2 clients before #1342 sent one for a\n  relative URI.\n- HeaderDecompression: reject a non-pseudo field name containing \u0027:\u0027 (RFC 9113 8.2.1).\n\nResult:\nThese requests are rejected with a 400/505 for the affected request or stream; an\ninvalid \u0027:scheme\u0027 no longer tears down every stream on the connection.\n\nTests:\n- Without the main changes the new RequestParserSpec CR/LF test and the RequestParsingSpec\n  \u0027:\u0027 name and \u0027:scheme\u0027 tests fail\n- sbt http-core/test: 1331 passed\n- sbt http2-tests/test: 390 passed\n- sbt http-core/mimaReportBinaryIssues and scalafmt checks: pass\n\nReferences:\nRefs #1087"
    },
    {
      "commit": "0808b90b2c507708bfe76fed6d55db3c2dee2818",
      "tree": "0181d3609208c1ebcab9c03fcfd5acd1b8d9759e",
      "parents": [
        "d81fa589b493ba08f056fb778e071755a934576c"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Oct 13 10:32:54 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 10:32:54 2026 +0100"
      },
      "message": "fix: send \u0027:scheme\u0027 and \u0027:authority\u0027 for a relative URI over HTTP/2 (#1342)\n\nMotivation:\nThe HTTP/2 client rendered \u0027:scheme\u0027 and \u0027:authority\u0027 straight from the request URI, so a\nrequest with a relative URI - the usual form with the connection-level client API -\nwas sent with both empty. RFC 9113 8.3.1 makes \u0027:scheme\u0027 mandatory and forbids an empty\n\u0027:authority\u0027. A Pekko HTTP server saw the request URI as `/foo` rather than\n`http://host:port/foo`, and stricter servers may reject the request.\n\nModification:\n- RequestRendering takes the connection\u0027s scheme and authority and uses them when the\n  request URI has none; a non-empty Host header supplies the authority first, as it\n  does for HTTP/1.1. An absolute URI is rendered as before.\n- Http2.outgoingConnection passes `https` and outgoingConnectionPriorKnowledge `http`,\n  with the connection\u0027s host and port (port left out when it is the scheme\u0027s default).\n- Http2Blueprint.clientStack and httpLayerClient keep their existing signatures, which\n  render as before, and gain overloads taking the connection scheme and authority.\n\nResult:\nRequests with a relative URI carry a valid \u0027:scheme\u0027 and \u0027:authority\u0027.\n\nTests:\n- Without the main change the new HttpMessageRenderingSpec relative-URI and Host\n  header tests and the WithPriorKnowledgeSpec end-to-end test fail\n- sbt http2-tests/test: 390 passed\n- sbt http-core/mimaReportBinaryIssues: pass\n- scalafmt applied to http-core and http2-tests\n\nReferences:\nNone - follow-up to #1341, which found the Pekko client sending an empty \u0027:scheme\u0027"
    },
    {
      "commit": "d81fa589b493ba08f056fb778e071755a934576c",
      "tree": "53a1f1aa4773468ced70579ea7f1dbd54ea1502e",
      "parents": [
        "9a138bad2b2cda9e03a92311509d73eeeb61055b"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Oct 13 05:35:59 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 13 05:35:59 2026 +0100"
      },
      "message": "fix: stop public Scala APIs requiring a private JavaMapping implicit (#1340)\n\nMotivation:\nHttpMessage.attribute and the ResponseEntity.fromJava implicit conversion took an\nimplicit JavaMapping, which is private[http]. Scala 3.9 warns when user code resolves\nit (\"Usage of implicit ... which is not accessible here\") and Scala 3.10 will stop\nfinding it, so request.attribute(AttributeKeys.remoteAddress) would no longer compile.\n\nModification:\n- Look up the JavaMapping inside attribute and fromJava instead of taking it as a\n  parameter, matching addAttribute and removeAttribute.\n- Escalate the \"which is not accessible here\" warning to an error on Scala 3; the\n  deprecation category it belongs to is otherwise silenced.\n- Add HttpModelIntegrationSpec cases, from outside org.apache.pekko.http, that read an\n  attribute and convert a javadsl ResponseEntity.\n- Add MiMa filters for the removed implicit parameter lists and for the internal\n  JavaMapping types that no longer escape into the public API.\n\nResult:\nUser code can call attribute and use the ResponseEntity conversion on Scala 3.10.\nThis changes the binary signature of both methods, which is acceptable for 2.0.0.\n\nTests:\n- sbt \"++3.9.0!\" http-core/Test/compile without the main change: fails with the\n  inaccessible-implicit error at the three new call sites\n- sbt \"++3.9.0!\" Test/compile (all modules) and HttpModelIntegrationSpec: pass\n- HttpModelIntegrationSpec on 2.13.18 and 3.3.8: pass\n- sbt http-core/mimaReportBinaryIssues: pass\n- sbt http-core/scalafmtCheckAll http-core/Test/scalafmtCheck scalafmtSbtCheck: pass\n\nReferences:\nFixes #1056"
    },
    {
      "commit": "9a138bad2b2cda9e03a92311509d73eeeb61055b",
      "tree": "1ab3055f3cbc20d80043674bebaf06908b261250",
      "parents": [
        "80862428daf55643aecb051dedfda92122caab5e"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Mon Oct 12 22:09:52 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 12 22:09:52 2026 +0100"
      },
      "message": "Drop chunk extensions containing NUL, not only CR or LF (#1344)\n\n* Drop chunk extensions containing NUL, not only CR or LF\n\nMotivation:\nrenderChunk writes a chunk extension raw into the chunk-size line and\ndrops it if it contains CR or LF, but not NUL. Since #1260 the header\nand trailer renderers drop all three via Rendering.isIllegalHeaderChar,\nso a NUL in an application-supplied chunk extension was the one place\nit still reached the wire. Review of the threat model in #1295 found\nthe gap: P2 had to be narrowed to exclude chunk extensions from its\nNUL claim.\n\nModification:\nCheck the extension with Rendering.isIllegalHeaderChar, the predicate\nthe header and trailer paths use. Add a ResponseRendererSpec case\nbeside the existing CRLF one.\n\nResult:\nA chunk extension containing CR, LF or NUL is dropped on both the\nserver response and client request paths, which share renderChunk.\n\nTests:\n- sbt \"http-core / Test / testOnly ...ResponseRendererSpec -- -z NUL\": failed before the fix (rendered \"7;ok\\0bad\")\n- sbt \"http-core / Test / testOnly ...ResponseRendererSpec ...RequestRendererSpec\": 63 passed\n- scalafmt --mode diff-ref\u003dupstream/main: no changes\n\nReferences:\nRefs #1256, #1260, #1295\n\n* Explain why NUL is dropped from chunk extensions and reword test comment"
    },
    {
      "commit": "80862428daf55643aecb051dedfda92122caab5e",
      "tree": "580686e6a536e5a5715af30eb417ba888db12b59",
      "parents": [
        "67597108e7e2eec22560bd6aed0436e89bbf540b"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Mon Oct 12 17:26:07 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 12 17:26:07 2026 +0100"
      },
      "message": "build(deps): Bump actions/upload-artifact from 7.0.1 to 7.0.2 (#1347)\n\nBumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.1 to 7.0.2.\n- [Release notes](https://github.com/actions/upload-artifact/releases)\n- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)\n\n---\nupdated-dependencies:\n- dependency-name: actions/upload-artifact\n  dependency-version: 7.0.2\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "67597108e7e2eec22560bd6aed0436e89bbf540b",
      "tree": "bbb2b0dea11bf157973dfe71100f30beb4991a0c",
      "parents": [
        "64752d44a45cb5d07fa33ff857f37854bab2a45d"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Mon Oct 12 18:25:38 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Oct 12 17:25:38 2026 +0100"
      },
      "message": "Update classgraph to 4.8.198 (#1348)"
    },
    {
      "commit": "64752d44a45cb5d07fa33ff857f37854bab2a45d",
      "tree": "1b3f7c34a89d61cfc5c46257884e916a37c8f0ee",
      "parents": [
        "8b90944b7dc68b6d6bef711d6709edaaffe2ecb7"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Oct 11 17:23:25 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Oct 11 17:23:25 2026 +0100"
      },
      "message": "fix: reject uppercase HTTP/2 field names and values with surrounding whitespace (#1343)\n\n* fix: reject uppercase HTTP/2 field names and values with surrounding whitespace\n\nMotivation:\nRFC 9113 8.2.1 makes an HTTP/2 message malformed when a field name contains an\nuppercase character or a field value starts or ends with SP or HTAB. Pekko HTTP accepted\nboth: regular fields are handed to the HTTP/1.1 header-line parser, which matches names\ncase-insensitively and trims the value, so the message was silently normalised instead of\nrejected. Other HTTP/2 implementations (nghttp2, netty for names) reject them, so two hops\ncan disagree about the same field.\n\nOn the client, a field rejected in HeaderDecompression left the response with no headers\nand was reported as a missing \u0027:status\u0027 rather than as what was wrong.\n\nModification:\n- HeaderDecompression rejects an uppercase field name and a value with leading or\n  trailing SP/HTAB, next to the existing NUL/CR/LF check. The messages from this stage\n  drop the \"Malformed request:\" prefix, since it decodes responses on the client too.\n- ResponseParsing reports a header error from decompression as\n  \"Malformed response: \u003creason\u003e\".\n- Lowercase the incidental uppercase names (\"Foo\", \"Connection\", \"TE\") in RequestParsingSpec\n  tests that check other rules, and let the \u0027:authority\u0027/\u0027:path\u0027 tests that pass\n  whitespace-only or whitespace-terminated values accept the earlier field-check message.\n- Add a ResponseParsingSpec for the client side.\n\nResult:\nRequests with such fields get a 400 for the stream; responses with them fail with a\nprotocol error naming the field problem. Pekko HTTP\u0027s own client and server always render\nlowercase names, so traffic between them is unaffected.\n\nTests:\n- Without the main change the new RequestParsingSpec uppercase/whitespace tests and the\n  ResponseParsingSpec rejection tests fail\n- sbt http2-tests/test: 393 passed\n- sbt http-core/mimaReportBinaryIssues and scalafmt checks: pass\n\nReferences:\nNone - follow-up to #1341\n\n* scalafmt\n\n* reject every character RFC 9113 8.2.1 excludes from field names, not just uppercase"
    },
    {
      "commit": "8b90944b7dc68b6d6bef711d6709edaaffe2ecb7",
      "tree": "c3e03a87bb512b05000aca8c7b4638a9d3841ae1",
      "parents": [
        "179ea04c00330e64ff24e5378acad88c7cdada9a"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Sun Oct 11 10:19:03 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Oct 11 10:19:03 2026 +0100"
      },
      "message": "build(deps): Bump sbt/setup-sbt from 1.5.8 to 1.5.11 (#1321)\n\nBumps [sbt/setup-sbt](https://github.com/sbt/setup-sbt) from 1.5.8 to 1.5.11.\n- [Release notes](https://github.com/sbt/setup-sbt/releases)\n- [Commits](https://github.com/sbt/setup-sbt/compare/c7d2d6258b4bd0d3ec5129e6b3453199d3c79729...6158cb0903b8ceeae04f830055f3155e1b6a5ad7)\n\n---\nupdated-dependencies:\n- dependency-name: sbt/setup-sbt\n  dependency-version: 1.5.11\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "179ea04c00330e64ff24e5378acad88c7cdada9a",
      "tree": "e10c79d2754d2e0486e380bac8f784bfaf826fef",
      "parents": [
        "5954bdf94fa2fc0e1495126ed8f68a39041cea09"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Oct 11 09:13:37 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Oct 11 09:13:37 2026 +0100"
      },
      "message": "docs: fix and add release note milestone links (#1339)\n\n* docs: fix release note links (1.1.1 milestone, swapped PR616/PR630, issue #1133)\n\nCo-Authored-By: Claude Opus 5.5 \u003cnoreply@anthropic.com\u003e\n\n* docs: add 1.0.1 milestone link and 1.2.1 milestone link for 1.3.0\n\nCo-Authored-By: Claude Opus 5.5 \u003cnoreply@anthropic.com\u003e\n\n---------\n\nCo-authored-by: Claude Opus 5.5 \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "5954bdf94fa2fc0e1495126ed8f68a39041cea09",
      "tree": "77a8890bf1f5e692c200216a05b5b51eabf33f4e",
      "parents": [
        "b1de40b1abbc07fd4da19f72c89faa978e87df4b"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sat Oct 10 15:39:56 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Oct 10 15:39:56 2026 +0100"
      },
      "message": "Re-pin the threat model to b1de40b, absorb what merged, and trim it to what a triager needs (#1295)\n\n* Re-pin the threat model to 40b07a2 and absorb what merged since 6740cbd\n\nMotivation:\nNineteen commits landed on main after the model was pinned to `6740cbd`,\nten of which touch a claim it makes. One changes a triage line: #1194\ndocuments `idle-timeout` as a bidirectional inactivity timeout that a\nclient sending bytes inside every window keeps alive by design, where §9\ncalled such a connection an evasion and therefore in scope. Two others\ndefend a property the model never stated: that a malformed request on\none HTTP/2 stream is answered on that stream and leaves the connection,\nits HPACK state and its other streams alone (#1252, #1297).\n\nChecking the error path for #1246 also surfaced a shipped default the\nmodel never mentioned: `error-logging-verbosity \u003d full` writes the\nfailing request target into the log at warning level, independent of the\nclient-facing `verbose-error-messages \u003d off` that P3 rests on.\n\nModification:\nRe-pin to `40b07a2`. Add P9, HTTP/2 stream isolation, cited to\n`RequestErrorFlow`, with a note recording the two paths around it that\nreview found and closed: a header that failed to parse unwound out of\nthe HPACK decoder before the dynamic table was updated, desynchronising\nevery later HEADERS frame on the connection (#1252, merged through\n#1251); and a field the HTTP/1.1 header parser rejects was reported with\nan exception type nothing on the HTTP/2 side caught, failing the\nconnection (#1297, which also closed the RFC 9113 8.2.1 gap of a CR LF\nvalue being accepted silently truncated). Restate P5 and the §9\nslow-loris bullet around inactivity rather than evasion, citing the new\n`reference.conf` and `timeouts.md` wording, and record #1284\u0027s leak of\none scheduled task per open request as the in-scope shape. Add #1257 to\nthe §9 smuggling bullet beside #1267, and #1281 to the P1 note beside\n#1259 as the third discard path. Add `max-part-count` (#1266) to §5a and\na multipart row to §6, which had no multipart input at all, noting\n#1279\u0027s header-state bleed between parts. Add `error-logging-verbosity`\nto §5a, a §9 false-friend entry explaining what `verbose-error-messages`\ndoes not govern, and §10.9 recommending `simple` where logs are shipped\nor alerted on. Add two §11 misuse patterns: building `Raw-Request-URI`\nfrom request input, and echoing `IllegalRequestContext.rawRequestTarget`\nunescaped. Turn §5b.4\u0027s two concrete examples into a running tally of\nthe defects review has found of that shape. Extend §15 to match.\n\nResult:\nEvery claim is verified against `40b07a2`. Provenance is 23 documented /\n33 maintainer / 0 inferred. The P1 frame-size gap stays open; #1264 has\nnot merged.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1194, #1246, #1251, #1252, #1257, #1266, #1279, #1280, #1281, #1284, #1297\n\n* Trim the threat model to what a triager needs; the review history moves to the PR\n\nMotivation:\nThe document had grown to ten thousand words, and the growth was not\nin its claims but in narrative about how each claim came to be stated:\nfour \"how P-n came to be stated\" essays under the §8 table, §14 answers\nthat re-argued the reasoning the owning section had already absorbed,\n§9 bullets that explained each fixed defect\u0027s mechanism, a Confidence\nparagraph recounting which questions turned out to be corrections, and\nthe same fixes listed in three or four places each. Its readers are\ntriagers and the report-generating tools that cite it; they need the\nproperties, the limits, the dispositions and the non-findings, and they\nneed them fast. The review history is real but belongs in the pull\nrequest, not in the artefact reporters read.\n\nModification:\nCut to 6,500 words with no claim, ruling, disposition, table row or\ndefault removed. The four §8 notes become one verification paragraph\nnaming each defect and its PR in a clause; §14 is retitled \"Maintainer\nrulings\" and each answer reduced to its ruling, with the reasoning kept\nonly where it is the ruling (Q3\u0027s three tiers); §9\u0027s essays are cut to\nthe boundary they draw plus one PR reference per fixed case; the\nConfidence paragraph is replaced by one sentence; the §5b.4 tally, the\n§12 near-miss narrative and the §5 shutdown-hook caveat are each\nreduced to a sentence; line-number citations are dropped in favour of\nfile or symbol names, since the pin fixes the commit and the numbers\nrot on every merge. Every PR a claim depends on is still referenced,\nonce.\n\nResult:\nSame model, 36% shorter, with §14 no longer presenting settled rulings\nas open questions.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1295\n\n* Re-pin the threat model to 478c58b and absorb what merged since 40b07a2\n\nMotivation:\nTwenty commits landed on main after the model was pinned to `40b07a2`.\nTwo touch a claim it makes. #1264 closes the one P1 gap the model\nrecorded as open: the HTTP/2 frame parser now rejects a frame over\n`max-frame-size` (512kB) on its frame header, before buffering the\npayload. #1296 validates the two application-supplied parts of the\nrequest line at construction, which changes what the §11\n`Raw-Request-URI` misuse can reach.\n\nThe rest do not move a claim: #1316 adds `max-connection-age`, default\n`infinite`; #1301 encodes HPACK literals as ISO-8859-1, which still\nsubstitutes \u0027?\u0027 above 0xFF, and the P2 guard checks the rendered bytes;\n#1305, #1298, #1318 and the dependency updates are not security-relevant.\n\nModification:\nRe-pin to `478c58b`. Add `max-frame-size` to §5a, §6 and the §15 back-map,\nadd #1264 to the P1 verification paragraph, and drop the \"one P1 gap is\nopen\" paragraph. Restate the §11 `Raw-Request-URI` misuse: injection is\nnow rejected at construction, what remains is client bytes choosing an\nunnormalized target, and a value that passes construction yet corrupts\nthe request line is `VALID` under §5b.4. Add the matching §15 row.\n\nResult:\nThe model is verified against `478c58b` and records no open P1 gap.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1264, #1296\n\n* Narrow threat-model claims the code does not back\n\nMotivation:\nReview of #1295 checked five claims against the code and found each\nbroader than the implementation. P9 promised a per-stream 400 for any\nmalformed HTTP/2 request, but pseudo-header and connection-specific\nheader violations go through RequestParsing.protocolError, which throws\nHttp2ProtocolException and closes the connection with GOAWAY. P2\npromised NUL filtering on chunk extensions, but RenderSupport.renderChunk\ndrops only extensions containing CR or LF. P5 said request-timeout starts\nonce a request is received; RequestTimeoutSupport schedules it only after\nthe entity completes. §6 described rawRequestTarget as the bytes that\nfailed to parse, but it is populated whenever the target was read,\nincluding when the failure is a later header. The §9 log-hygiene claim\ncited ErrorInfo.scala, which does not establish the logging chain.\n\nModification:\nNarrow P9 and its §15 row to model-level field parsing failures and name\nthe GOAWAY class. Restrict P2\u0027s NUL claim to headers and trailers. Scope\nP5\u0027s request-timeout to a fully received entity. Reword the §6\nrawRequestTarget row. Cite UriParser.fail, DefaultParsingErrorHandler\nand logParsingError for the error-logging-verbosity claim in §9 and §15.\n\nResult:\nEvery changed claim matches the code at the pinned commit.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1295\n\n* Rule HTTP/2 framing violations that close the connection out of scope\n\nMotivation:\nNarrowing P9 left a gap in triage: the model now says that a missing,\nduplicate, misplaced or unknown pseudo-header, or a connection-specific\nheader, closes the connection with GOAWAY rather than the stream error\nRFC 9113 8.1.1 asks for, but gives a report of that no disposition.\n\nModification:\nAdd §14 Q11: only the violating peer\u0027s connection is lost, a\nmultiplexing proxy owns producing conformant pseudo-headers, and a report\nis BY-DESIGN: property-disclaimed; moving to RST_STREAM is a compliance\nimprovement for an ordinary issue. Add the matching §11a non-finding and\npoint P9 at the ruling.\n\nResult:\nA report of this shape routes to a disposition instead of MODEL-GAP.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1295\n\n* Re-pin the threat model to b1de40b\n\nMotivation:\nNine commits landed on main after the model was pinned to `478c58b`.\nNone moves a claim. #1320 and #1323 add the client-only\n`persistent-connection-max-age`, default `infinite`, and reword a\ncomment on the server\u0027s `max-connection-age` drain, a setting the\nmodel does not cite. The `Http2Blueprint` change only adds\nparentheses; the stage order is unchanged. The rest are syntax cleanups,\nrelease notes, build and dependency updates.\n\nModification:\nRe-pin to `b1de40b`.\n\nResult:\nThe model, including the claims narrowed in review, is verified against\n`b1de40b`.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1295, #1320, #1323\n\n* Update THREAT_MODEL.md\n\nCo-authored-by: Samuele Resca \u003csamuele.resca@gmail.com\u003e\n\n* State that verify does not hide the secret\u0027s length\n\nMotivation:\nReview of #1295 noted that `secure_\u003d\u003d` returns early on a length\nmismatch, which its own scaladoc documents as leaking whether the two\nsecrets have the same length, while P8 mentioned the length check\nwithout its consequence. The applied Client IP suggestion also left\ntrailing whitespace and a missing space before a citation.\n\nModification:\nState the length disclosure in P8, noting that `verify(secret, hasher)`\nover fixed-length digests removes it. Add to §14 Q5 that a length-only\nreport describes documented behaviour, not a P8 violation. Tidy the\nClient IP bullet.\n\nResult:\nP8 matches `EnhancedByteArray.secure_\u003d\u003d` as documented, and a\nlength-disclosure report has a disposition.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1295\n\n---------\n\nCo-authored-by: Samuele Resca \u003csamuele.resca@gmail.com\u003e"
    },
    {
      "commit": "b1de40b1abbc07fd4da19f72c89faa978e87df4b",
      "tree": "a69eaa4d418a7b40e92f6a7220fe32c8da8ba38a",
      "parents": [
        "cf0233879578d5a471f6d74aec088ec2f9863dfd"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Thu Oct 08 18:26:27 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Oct 08 17:26:27 2026 +0100"
      },
      "message": "Update classgraph to 4.8.197 (#1336)"
    },
    {
      "commit": "cf0233879578d5a471f6d74aec088ec2f9863dfd",
      "tree": "39bb90fc5106b09d32b07c8b7bfc681936341b64",
      "parents": [
        "ef1eca76a85ae1ea2098abf411932dee159eabee"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Wed Oct 07 13:55:19 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Oct 07 13:55:19 2026 +0100"
      },
      "message": "Fail Scala 3.8+ builds on warnings again (#1333)\n\nMotivation:\nThe Scala 3.8+ lane added `-Wconf:any:s`, which silenced every warning and\nmade `-Werror` a no-op there. As a result `private[this]` (deprecated in\nScala 3) crept back into ByteStringOutputStream unnoticed.\n\nModification:\n- Replace `-Wconf:any:s` with filters for the four migration warnings that\n  have no replacement also compiling on Scala 2.13 (`using` clauses, `xs*`\n  vararg splices, trailing `_` eta-expansion, `with` as a type operator).\n- Replace `private[this]` with `private` in ByteStringOutputStream.\n- Convert alphanumeric infix calls not declared `infix` to dot calls.\n\nResult:\nOther Scala 3.8+ warnings, such as `private[this]`, fail the build again.\n\nTests:\n- sbt \"++3.9.0!\" Test/compile http-tests/MultiJvm/compile: success\n- sbt \"++2.13.18!\" Test/compile http-tests/MultiJvm/compile: success\n- Reintroducing `private[this]` fails `sbt \"++3.9.0!\" http-core/compile`\n  under -Werror\n\nReferences:\nNone - follow-up to #1129"
    },
    {
      "commit": "ef1eca76a85ae1ea2098abf411932dee159eabee",
      "tree": "339d224a35d6748f23004941354e2eb63bfe54bb",
      "parents": [
        "7448e899655d4351c7d37ff22b3e00a2b09bb897"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Oct 06 12:42:56 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 06 12:42:56 2026 +0100"
      },
      "message": "release notes for 1.4.1 (#1277)\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "7448e899655d4351c7d37ff22b3e00a2b09bb897",
      "tree": "d54226f7c98a91e3ccc96dfb1a7bd1fb74e7258c",
      "parents": [
        "243329f90688d8e3a55ba75b541d162587353398"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Oct 06 12:30:14 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 06 12:30:14 2026 +0100"
      },
      "message": "2.0.0-M2 release notes (#1326)\n\n* 2.0.0-M2 release notes\n\n* Apply suggestions from code review\n\nCo-authored-by: Philippus Baalman \u003cphilippus@gmail.com\u003e\n\n---------\n\nCo-authored-by: Philippus Baalman \u003cphilippus@gmail.com\u003e"
    },
    {
      "commit": "243329f90688d8e3a55ba75b541d162587353398",
      "tree": "acbc18368f75b2afc47b10961e0208a0e22ce8c2",
      "parents": [
        "561929f9659961d89ed7e6e5b97b701362c5a832"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Tue Oct 06 12:29:54 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Oct 06 12:29:54 2026 +0100"
      },
      "message": "build(deps): Bump coursier/setup-action from 3.0.3 to 3.0.4 (#1331)\n\nBumps [coursier/setup-action](https://github.com/coursier/setup-action) from 3.0.3 to 3.0.4.\n- [Release notes](https://github.com/coursier/setup-action/releases)\n- [Commits](https://github.com/coursier/setup-action/compare/3174df5c784a0c8724f06b7924b849551c40c39e...648df969f41ef15fda2baba8b37f9fa3d16390a3)\n\n---\nupdated-dependencies:\n- dependency-name: coursier/setup-action\n  dependency-version: 3.0.4\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "561929f9659961d89ed7e6e5b97b701362c5a832",
      "tree": "1ab0f13347055dd773b4855c898c372505599210",
      "parents": [
        "9c63776a983bce89ea93a666b94f8e5d0cb42612"
      ],
      "author": {
        "name": "Rayan-and-beyond",
        "email": "2220000455@iau.edu.sa",
        "time": "Sat Oct 03 13:58:36 2026 +0300"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Oct 03 11:58:36 2026 +0100"
      },
      "message": "feat: add client-side max connection age for HTTP/2 (#1320)\n\n* http2: add persistent client connection max age #1319\n\nMotivation:\nLong-lived managed HTTP/2 clients can stay pinned to existing server instances.\n\nModification:\nAdd a configurable maximum age that retires managed persistent HTTP/2 connections after in-flight requests drain.\n\nResult:\nRequests arriving after retirement establish a fresh connection while in-flight requests complete normally.\n\nTests:\n- sbt validatePullRequest\n- sbt http2-tests/test\n- sbt +http-core/mimaReportBinaryIssues\n- sbt scalafmtCheckAll scalafmtSbtCheck\n- sbt +headerCheckAll\n- sbt docs/paradox\n- sbt checkCodeStyle\n- git diff --check\n\nReferences:\nRefs #1319\n\n* http2: address max-age review feedback\n\nExpose client max-age and jitter as public settings, add per-connection jitter, and preserve a buffered request when the request source completes during retirement. Document break-before-make behavior and extend retirement/reconnect coverage.\n\n* http2: preserve Java max-age precision\n\n* Align client max connection age with the server-side setting from #1316\n\nMotivation:\n#1316 merged with `infinite` as the disabled value for\n`max-connection-age`, a `Duration` setting type and\n`JavaDurationConverter` for the Java API. The client setting used `0s`\nand `FiniteDuration`.\n\nModification:\n- `persistent-connection-max-age` defaults to `infinite`, is a\n  `Duration`, and must be \u003e 0 or `infinite`, as on the server\n- Java accessors use `JavaDurationConverter`, so\n  `ChronoUnit.FOREVER.getDuration` round-trips to `Duration.Inf`\n- reference.conf, scaladoc and docs follow the server wording\n- the jitter scheduling matches `Http2Demux`\n- settings tests move to a new `Http2ClientSettingsSpec`, mirroring\n  `Http2ServerSettingsSpec`\n- MiMa excludes are reduced to the abstract members that need them\n- pekko-style imports in the changed files\n\nResult:\nThe client and server max connection age settings share naming\nconventions, defaults, validation and Java conversion behaviour.\n\nTests:\n- sbt \"http-core/testOnly ...Http2ClientSettingsSpec ...Http2CommonSettingsSpec ...Http2ServerSettingsSpec\": 16 passed\n- sbt \"http2-tests/testOnly ...Http2PersistentClient*\": 26 passed\n- sbt \"http-core/mimaReportBinaryIssues\": clean\n- sbt scalafmtAll and headerCreateAll run on changed modules\n\nReferences:\nRefs #1319, #1316\n\n---------\n\nCo-authored-by: PJ Fanning \u003cpjfanning@users.noreply.github.com\u003e"
    },
    {
      "commit": "9c63776a983bce89ea93a666b94f8e5d0cb42612",
      "tree": "fbfc7a17dad8766fad387b710c4ff1c540b50fa1",
      "parents": [
        "958943d2f3a1cf1fce8084689aef9835f4bf4f5a"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sat Oct 03 11:32:26 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Oct 03 11:32:26 2026 +0100"
      },
      "message": "Follow-up to max-connection-age: infinite grace test, docs, and FOREVER round-trip in Java setters (#1323)\n\n* Follow-up to max-connection-age review: document and test infinite grace with a later terminate\n\nMotivation:\nReview of #1316 noted that reference.conf only documents one direction of\nthe interaction between max-connection-age and a server binding termination,\nthat the combination of max-connection-age-grace \u003d infinite with a later\nterminate was not tested, and that some new tests had tight timing margins.\n\nModification:\n- reference.conf: state that a later terminate with an earlier deadline\n  shortens a drain started by the age, also with an infinite grace period,\n  mirroring http2.md.\n- Http2ServerSpec: add a test for max-connection-age-grace \u003d infinite\n  followed by terminate(10.millis).\n- Http2ServerSpec: widen timing margins (age 1s vs expectNoBytes(700ms),\n  terminate(2s) vs expectNoBytes(700ms)).\n\nResult:\nThe documented and tested behaviour covers both directions, and the timing\nsensitive tests have more headroom on loaded CI machines.\n\nTests:\n- sbt \"http2-tests/testOnly ...Http2ServerSpec -- -z max-connection-age\": 7 tests pass\n- scalafmt on the changed spec\n\nReferences:\nRefs #1316\n\n* Map ChronoUnit.FOREVER back to Duration.Inf in the other Java duration setters\n\nMotivation:\nGetters such as ConnectionPoolSettings.getKeepAliveTimeout return\nChronoUnit.FOREVER.getDuration for an infinite value, but the matching Java\nsetters converted with toScala, which throws IllegalArgumentException for\nthat value. Passing a getter\u0027s result back to its setter failed on the\ndefault settings of keep-alive-timeout, max-connection-lifetime and\nperiodic-keep-alive-max-idle.\n\nModification:\nUse JavaDurationConverter.toScala, the inverse of the toJava used by the\ngetters, in the Java setters of ClientConnectionSettings.idleTimeout,\nConnectionPoolSettings idleTimeout, keepAliveTimeout, maxConnectionLifetime\nand responseEntitySubscriptionTimeout, and\nWebSocketSettings.periodicKeepAliveMaxIdle.\n\nResult:\nInfinite durations round-trip through the Java API.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.scaladsl.settings.*\": 27 pass;\n  the 3 new round-trip tests fail without the fix\n- sbt \"http-core/mimaReportBinaryIssues\": clean\n\nReferences:\nRefs #1316"
    },
    {
      "commit": "958943d2f3a1cf1fce8084689aef9835f4bf4f5a",
      "tree": "3ec17e86b2f545adca44892e3f2e33b8babe9a2f",
      "parents": [
        "5c7e50fbae2c9c4071120a41446687ee2fc5097e"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Fri Oct 02 18:01:33 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 02 17:01:33 2026 +0100"
      },
      "message": "Update sbt-paradox, sbt-paradox-theme to 0.11.1 (#1325)"
    },
    {
      "commit": "5c7e50fbae2c9c4071120a41446687ee2fc5097e",
      "tree": "31cb8d1d2c16cdcdc9c0f95beb2d4e019e35c268",
      "parents": [
        "478c58bb733fadbe921660bccc964311ad58e9ef"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Fri Oct 02 18:01:25 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 02 17:01:25 2026 +0100"
      },
      "message": "Update sbt-java-formatter to 0.14.0 (#1324)"
    },
    {
      "commit": "478c58bb733fadbe921660bccc964311ad58e9ef",
      "tree": "6f72e4559809b06de64c37384f70a2b710f6943d",
      "parents": [
        "638a214dccb4629de876c26b5cac998bfc5d8f49"
      ],
      "author": {
        "name": "Anders Kreinøe",
        "email": "anders@kreinoee.dk",
        "time": "Fri Oct 02 11:20:19 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 02 10:20:19 2026 +0100"
      },
      "message": "feat: add max-connection-age setting for HTTP/2 server connections (#1316)\n\n* feat: add max-connection-age setting for HTTP/2 server connections\n\nMotivation:\nLong-lived HTTP/2 connections (as used by gRPC) lead to an uneven load\ndistribution across server instances: clients stay connected to the\ninstances they found at connect time, and instances added later (after\na scale-out or a rolling deploy) receive no share of the existing\ntraffic. The server is the side that can retire a connection\ngracefully, via GOAWAY. grpc-java offers this as maxConnectionAge;\npekko-http has no equivalent (akka/akka-grpc#967 is the corresponding\nrequest on the Akka side).\n\nModification:\nAdd a `pekko.http.server.http2.max-connection-age` setting, default\n`infinite` (disabled). When a server connection reaches the configured\nage, the existing graceful termination path is triggered:\nGOAWAY(NO_ERROR) is sent, streams that are in flight complete normally,\nstreams opened after the GOAWAY are refused with\nRST_STREAM(REFUSED_STREAM), and the connection is closed once no\nstreams remain. The age is jittered per connection by a configurable\nfraction, `max-connection-age-jitter` (default 0.1 \u003d +/- 10%, the value\ngrpc-java applies; 0 disables jitter), so that connections that were\nopened together are not all closed at the same time.\n`triggerTermination` now accepts an infinite deadline, in which case no\nforced-close timer is scheduled. Also corrects the termination debug\nlog, which printed the timer key instead of the deadline.\n\nResult:\nOperators can cap the lifetime of server-side HTTP/2 connections to\nrebalance long-lived connections across server instances. Behavior is\nunchanged by default.\n\nTests:\n- sbt \"http2-tests/test\": 376 tests pass, including 2 new directional\n  tests for max-connection-age in Http2ServerSpec\n- sbt validatePullRequest: passes\n- sbt \"+http-core/mimaReportBinaryIssues\": clean, with 5 new\n  ReversedMissingMethodProblem filters for the added methods\n- sbt checkCodeStyle: clean; sbt headerCreateAll: no changes\n- sbt docs/paradox: builds, remaining warnings pre-existing\n- sbt sortImports: environment failure unrelated to this change\n  (http/scalafixAll fails with NoSuchMethodError in scala.meta on a\n  clean checkout of main too); the files changed here are sort-clean\n- manual end-to-end check against grpc-java 1.75.0: with\n  max-connection-age \u003d 5s, a client calling every 200 ms for 16 s saw\n  0 failures across 3 connection retirements, and a unary call that\n  was in flight when the GOAWAY was sent completed normally\n\nReferences:\nNone - no pekko-http issue tracks this; akka/akka-grpc#967 is the\nequivalent request against akka-http/akka-grpc.\n\n* Set @since of the new settings to 1.4.2 as requested in review\n\n* Revert \"Set @since of the new settings to 1.4.2 as requested in review\"\n\nThis reverts commit 9ce450d826db93c02a9089d8a882d449456b25ee.\n\n* Map ChronoUnit.FOREVER back to Duration.Inf in the Java withMaxConnectionAge\n\nwithMaxConnectionAge(getMaxConnectionAge) threw ArithmeticException on the\ndefault settings: toMillis overflows on ChronoUnit.FOREVER.getDuration, the\nvalue getMaxConnectionAge returns for an infinite age. Adds the inverse of\nJavaDurationConverter.toJava and a round-trip test.\n\n* Document when JavaDurationConverter.toScala throws\n\n* Make the infinite round-trip test independent of the default max-connection-age\n\n* Add max-connection-age-grace to bound the drain after max-connection-age\n\nThe drain started by the MaxConnectionAge timer had no deadline, so a\nstream that never completes kept the connection open indefinitely. The\nnew setting bounds it, with a default of 30s. The value infinite keeps\nthe previous behaviour of waiting for all requests in flight to complete.\n\n* Drop the grpc-java aside from the max-connection-age-grace comment\n\n* Let a later terminate shorten a termination that is already in progress\n\ntriggerTermination ignored every call after the first, so a server binding\ntermination could not enforce its deadline on a connection that was already\ndraining after max-connection-age. Now an earlier deadline reschedules the\nforced close, a later one is ignored. The max-connection-age timer is\ncancelled once any termination starts, so the age and its grace period never\nshorten a termination in progress.\n\n* Guard the max-connection-age timer instead of cancelling it when a termination starts\n\nSame behaviour, but the rule is visible where it applies: the timer handler\nchecks whether a termination is already in progress and logs that there is\nnothing to do, rather than relying on the cancelled timer\u0027s message being\ndropped by the stage.\n\n* Name scheduleForcedCloseIfEarlier after what it does instead of commenting it\n\n* Say what an age expiry does on a connection that is already terminating, next to the age setting\n\n* State that max-connection-age applies to HTTP/2 only, and reword the jitter test comment\n\n* Do not point server-side forced-close logging at the client-only completion-timeout setting\n\nThe message is shared by both sides. On the client the deadline is the\ncompletion-timeout setting, on the server it is the deadline passed to\nterminate or the max-connection-age-grace setting.\n\n* Do not blame the peer in the server-side forced-close log message"
    },
    {
      "commit": "638a214dccb4629de876c26b5cac998bfc5d8f49",
      "tree": "ddff407cb9fa0cef8a9f38ce75edb93d34008f36",
      "parents": [
        "ff919e1cb39ace13afefc8eaf232c982541cfb71"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Thu Oct 01 19:26:33 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Oct 01 19:26:33 2026 +0100"
      },
      "message": "feat: bound the size of an incoming HTTP/2 frame (#1264)\n\n* feat: bound the size of an incoming HTTP/2 frame\n\nMotivation:\n`Http2FrameParsing` read a frame\u0027s 24-bit length field and then took\nthat many bytes with no upper bound, so a peer could declare a length\nup to the field\u0027s maximum of 16 MiB - 1 and make the frame parser\nbuffer that much for a single frame. That happens before the HPACK\nand entity limits apply, so `max-header-list-size`,\n`incoming-stream-level-buffer-size` and `incoming-connection-level-\nbuffer-size` do not bound it, and it is multiplied by the number of\nconnections.\n\nModification:\nAdd a `max-frame-size` setting to the HTTP/2 server and client\nsettings, defaulting to 512kB, and reject a larger frame with a\nFRAME_SIZE_ERROR on its frame header, before the payload is buffered.\nThe value is validated against the bounds RFC 9113, section 4.2 sets\nfor SETTINGS_MAX_FRAME_SIZE, 16 KiB to 16 MiB - 1.\n\nThe setting is a limit on what is accepted, not an advertisement. An\nearlier revision did advertise it as SETTINGS_MAX_FRAME_SIZE, which\nh2spec\u0027s \"4.2 Frame Size\" case showed to be actively harmful: DATA\nframes are flow controlled and the initial window is 64 KiB, so a peer\nthat sizes its first frame to a larger advertised value trips a\nFLOW_CONTROL_ERROR before the window has grown. Advertising nothing\nkeeps a well-behaved peer at the 16 KiB default it already assumes,\nand the configured value is then pure leniency for peers that exceed\nit - always accepting at least as much as any peer is told it may\nsend.\n\nResult:\nA single frame can no longer make the parser hold up to 16 MiB; the\nbound is 512kB by default and configurable. Frames from a spec\ncompliant peer are unaffected, and pekko\u0027s own existing behaviour of\naccepting frames well above 16 KiB is preserved.\n\nTests:\n- sbt \"http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.H2SpecIntegrationSpec\" - pass (236 tests), including the h2spec conformance suite. A new test sends a frame one byte over a configured 16 KiB limit and expects GOAWAY(FRAME_SIZE_ERROR); verified it fails with the size check disabled. The existing \"fail if more data is received than stream-level window allows\" test deliberately sends a single 512001 byte frame to exceed the stream buffer, so it now raises max-frame-size to 1 MiB to reach the flow-control check it is about.\n- sbt http-core/mimaReportBinaryIssues - pass\n- sbt \"+http-core/compile\" - pass on 2.13.18 and 3.3.8\n\nReferences:\nNone - bounds how much a single HTTP/2 frame can buffer\n\n* Add the Scala 3 MiMa excludes for the new max-frame-size members\n\nThe new abstract members on the HTTP/2 settings traits are only flagged\non Scala 3; the 2.13 check filters them under a broader rule, so a\nscoped run on the default Scala version alone did not surface them.\n\n* Address review: second oversized-frame test, MiMa filters, conf wording\n\nThe \"release connection-level flow control accounting when a\nstream-level window is exceeded\" test that #1281 added sends the same\nsingle 512001-byte DATA frame, so it also needs max-frame-size raised to\nreach the flow-control check it is about.\n\nMiMa filters now cover the concrete javadsl withMaxFrameSize and\ngetMaxFrameSize members, which MiMa still reports as\nReversedMissingMethodProblem, plus the problems the 2.13 check against\n1.0.0 surfaces after rebasing onto main: the replaced internal\nHttp2FrameParsing constructor, the abstract Http2CommonSettings member,\nand the copy$default$N shifts from the new case-class field.\n\nThe reference.conf comment pointed at the buffer-size settings as being\nabove max-frame-size; they are below it."
    },
    {
      "commit": "ff919e1cb39ace13afefc8eaf232c982541cfb71",
      "tree": "0af50e87bb9a9ca17554cee69d3c39107afd7b81",
      "parents": [
        "7d3ae9a3c663b71512c4bdfcb44f61d887bf6669"
      ],
      "author": {
        "name": "Tomasz Godzik",
        "email": "tgodzik@users.noreply.github.com",
        "time": "Thu Oct 01 18:13:08 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Oct 01 17:13:08 2026 +0100"
      },
      "message": "improvement: Use code for silencing warning (#1322)\n\nMessage changes in 3.3.9 and it stops being silenced."
    },
    {
      "commit": "7d3ae9a3c663b71512c4bdfcb44f61d887bf6669",
      "tree": "0066ea659ea968056861657194ad1e247a6615fc",
      "parents": [
        "fae3e0b746f8f54e66d964682024eb29195e7b87"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 25 14:29:07 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 25 14:29:07 2026 +0100"
      },
      "message": "Use 1.4.1 in @since tags for max-header-list-size settings (#1318)\n\n* Use 1.4.1 in @since tags for max-header-list-size settings\n\n* Move max-header-list-size MiMa excludes to 1.4.x.backwards.excludes"
    },
    {
      "commit": "fae3e0b746f8f54e66d964682024eb29195e7b87",
      "tree": "eebdd46d1807681e301cf0defc0072744354ff1b",
      "parents": [
        "07995950de2a1879a02c63dbb8e4c0d9ae33da1f"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Thu Sep 24 17:22:05 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 24 16:22:05 2026 +0100"
      },
      "message": "Update classgraph to 4.8.196 (#1315)"
    },
    {
      "commit": "07995950de2a1879a02c63dbb8e4c0d9ae33da1f",
      "tree": "d2a2f43b23ba6dce492b662302c739bb04e1509f",
      "parents": [
        "cbc42067d3372f24f133f6c449b14789a2419227"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Thu Sep 24 12:07:50 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 24 11:07:50 2026 +0100"
      },
      "message": "Update sbt-mima-plugin to 1.2.1 (#1313)\n\n* Update sbt-mima-plugin to 1.2.1\n\n* Add MiMa 1.2.1 no-longer-checked excludes\n\nMotivation:\nsbt-mima-plugin 1.2.1 reports members that became private[..] or sealed\nsince 1.0.0 as ClassNoLongerChecked, MethodNoLongerChecked and\nHierarchyNoLongerChecked problems, failing PR validation.\n\nModification:\nExclude the compression internals (Deflate/Gzip/NoCoding compressors,\nnewCompressor, withLevel) in http and the sealed HttpHeaderRange in\nhttp-cors. All were narrowed intentionally in 2.0.x.\n\nResult:\nMiMa passes with sbt-mima-plugin 1.2.1.\n\nTests:\n- sbt \"http/mimaReportBinaryIssues\" \"http-cors/mimaReportBinaryIssues\": passed (2.13)\n- sbt \"++3.x\" \"http/mimaReportBinaryIssues\" \"http-cors/mimaReportBinaryIssues\": passed\n\nReferences:\nRefs #1313\n\n---------\n\nCo-authored-by: PJ Fanning \u003cpjfanning@users.noreply.github.com\u003e"
    },
    {
      "commit": "cbc42067d3372f24f133f6c449b14789a2419227",
      "tree": "eb1b39edb9006804b416bb3513b24534139ee256",
      "parents": [
        "b82adfcad40eec296dceb17d3163a09a604093c5"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Wed Sep 23 08:06:10 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 23 08:06:10 2026 +0100"
      },
      "message": "perf: encode HPACK header blocks from byte arrays into an unsynchronized ByteStringOutputStream (#1301)\n\nMotivation:\nHuffmanEncoder read each string literal one char at a time through\nString.charAt, twice (once to size it, once to code it), and wrote its\noutput one byte at a time through OutputStream.write(int). Every write,\nincluding the integer prefixes and bulk literal writes from Encoder,\nwent through the synchronized ByteArrayOutputStream that\nHeaderCompression passes in, and the finished block was copied out with\ntoByteArray. The per-byte synchronized write dominated the cost of\nencoding a header block; the remaining per-header locks were another\nfifth of what was left.\n\nModification:\nHuffmanEncoder takes byte[] input, as the upstream twitter/hpack code\ndid, and codes straight into a reserved range of the output array.\nEncoder converts each string literal to octets once (via\nStringTools.asciiStringBytes, now ISO-8859-1 and so the exact inverse of\nasciiStringFromBytes: a raw literal carrying an octet in 0x80-0xFF sends\nthat octet where US-ASCII substituted \u0027?\u0027, as the Huffman form already\ndid) and uses them for the length check and both literal forms.\nByteStringOutputStream (#1235) is generalised into a plain OutputStream\nwith unsynchronized writes, a reserve/array pair for direct fills, and\ntakeByteString(), which hands the block over without copying when most\nof the buffer is used (starting the next block on a fresh array sized to\nthe last one) or copies out and keeps the array when only a small part\nis, as toByteStringUnsafe did. Encoder writes into that stream instead of\nany OutputStream; HeaderCompression and PerMessageDeflate use it. One\nMiMa excludes file covers the shaded Encoder and HuffmanEncoder\nsignatures.\n\nResult:\nEncoding a typical 10-header browser request is ~2.7x faster on JDK 17\n(about 9.6us to 3.5us, see the PR for the JMH table), both HPACK string\nliteral forms round-trip every octet through the decoder, and the\nWebSocket deflate stages no longer lock per write.\n\nTests:\n- sbt \"http-core/Test/testOnly org.apache.pekko.http.impl.util.ByteStringOutputStreamSpec org.apache.pekko.http.impl.util.StringToolsSpec org.apache.pekko.http.impl.engine.http2.hpack.HpackDecoderSpec org.apache.pekko.http.shaded.com.twitter.hpack.*\" (Scala 2.13.18 and 3.3.8)\n- sbt \"http-core/Test/testOnly org.apache.pekko.http.impl.engine.ws.*\"\n- sbt \"http2-tests/Test/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.RequestParsingSpec\"\n- sbt \"http-bench-jmh/Jmh/run HpackEncoderBenchmark\" on main and on this branch\n- sbt http-core/mimaReportBinaryIssues; sbt headerCheckAll; sbt javafmtCheckAll (JDK 17)\n- scalafmt --mode diff-ref\u003dupstream/main\n\nReferences:\nRefs #1235 - the ByteStringOutputStream this generalises; Refs #1300 - superseded by this PR"
    },
    {
      "commit": "b82adfcad40eec296dceb17d3163a09a604093c5",
      "tree": "d372553839538302af1abcdf768a3bf9486d455b",
      "parents": [
        "d7c59aae5c07d68d37507bee3a2bbe5c49b5acaa"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 22 17:23:21 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 22 16:23:21 2026 +0100"
      },
      "message": "Update jackson-databind to 2.22.3 (#1310)\n\nCo-authored-by: PJ Fanning \u003cpjfanning@users.noreply.github.com\u003e"
    },
    {
      "commit": "d7c59aae5c07d68d37507bee3a2bbe5c49b5acaa",
      "tree": "d6d6a6aa131bbb3378e087189fcf4a2e01490544",
      "parents": [
        "459cb1f2dfe46a157d708ca9ed050875c137061e"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 22 16:15:24 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 22 15:15:24 2026 +0100"
      },
      "message": "Update sbt-scalafix, scalafix-core to 0.14.9 (#1309)"
    },
    {
      "commit": "459cb1f2dfe46a157d708ca9ed050875c137061e",
      "tree": "f50a06c9d6b53ad004b6b7dabba0b3d8bc1d1f91",
      "parents": [
        "2fd621d60b1cb0ccb5c0026197aec7786352a3d1"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 22 16:14:09 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 22 15:14:09 2026 +0100"
      },
      "message": "Update sbt-api-mappings to 3.1.0 (#1312)"
    },
    {
      "commit": "2fd621d60b1cb0ccb5c0026197aec7786352a3d1",
      "tree": "0dcd69c7a571302be53ea3ea09d8217a56a8dffc",
      "parents": [
        "84a106198654e98091762d2f0e149a39a0ab8ce8"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 22 16:13:46 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 22 15:13:46 2026 +0100"
      },
      "message": "Update jackson-databind to 3.2.3 (#1314)"
    },
    {
      "commit": "84a106198654e98091762d2f0e149a39a0ab8ce8",
      "tree": "57aa563e685054235dc368ff456b526cca0c579c",
      "parents": [
        "0ed79fdcff704d183b84f5458fba96175f8c275a"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 22 16:13:26 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 22 15:13:26 2026 +0100"
      },
      "message": "Update caffeine to 3.3.0 (#1311)"
    },
    {
      "commit": "0ed79fdcff704d183b84f5458fba96175f8c275a",
      "tree": "09120216945cf0c4739d1a6102dec3e38957b078",
      "parents": [
        "f5c2bb560218c502b5e02fedab8dcb0d51691e1b"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Mon Sep 21 08:29:39 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 21 08:29:39 2026 +0100"
      },
      "message": "build(deps): Bump coursier/setup-action from 3.0.2 to 3.0.3 (#1308)\n\nBumps [coursier/setup-action](https://github.com/coursier/setup-action) from 3.0.2 to 3.0.3.\n- [Release notes](https://github.com/coursier/setup-action/releases)\n- [Commits](https://github.com/coursier/setup-action/compare/9b7939bf01fd1185ce2babe16135168361bf2c62...3174df5c784a0c8724f06b7924b849551c40c39e)\n\n---\nupdated-dependencies:\n- dependency-name: coursier/setup-action\n  dependency-version: 3.0.3\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "f5c2bb560218c502b5e02fedab8dcb0d51691e1b",
      "tree": "3446d9199585807f2aeda5c10348d4181dab7379",
      "parents": [
        "e697e5416bc52e072628d97288e9694911d265f9"
      ],
      "author": {
        "name": "Matthias Kurz",
        "email": "m.kurz@irregular.at",
        "time": "Fri Sep 18 17:56:02 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 18 16:56:02 2026 +0100"
      },
      "message": "build: replace external dependency graph plugin #452 (#1307)\n\nMotivation:\nThe legacy sbt-dependency-graph plugin is no longer needed because dependency-tree support is available in sbt itself.\n\nModification:\nUse addDependencyTreePlugin and derive PR validation module IDs directly from public UpdateReport data instead of legacy plugin graph types.\n\nResult:\nThe build uses sbt 1.13 dependency-tree support while preserving PR impact analysis without obsolete plugin APIs.\n\nTests:\n- sbt -batch \"show sbtVersion\" (passed; meta-build compiled with sbt 1.13.0)\n- Test suites not run per request.\n\nReferences:\nFixes #452"
    },
    {
      "commit": "e697e5416bc52e072628d97288e9694911d265f9",
      "tree": "88fec5166245af738ff8cd519beeebaaa71562c8",
      "parents": [
        "d9a5eb1e38c0c35a4a4d9ff80c8794f8f2ea5e5a"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 18 09:37:07 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 18 09:37:07 2026 +0100"
      },
      "message": "fix: reject a request-line part that cannot be sent as given (#1296)\n\n* Reject a Raw-Request-URI that cannot be sent as a request target\n\nMotivation:\n`Raw-Request-URI` is the escape hatch for sending a request target\nverbatim, and the HTTP/1.1 renderer honours that literally: it writes the\nvalue into the request line with `r ~~ rawUri`, with no check on what it\ncontains. Two things go wrong with an unchecked value. A space, CR or LF\nends the target early and lets whatever follows be read as the protocol,\na header or a second request, so an application that builds the header\nfrom input it did not validate has a request-line injection. And the\nrenderer writes a String one char at a time truncated to a byte, so a\ncharacter outside ASCII does not arrive as itself: U+010D lands on the\nwire as 0x0D, a CR, without the value ever containing one.\n\nOver HTTP/2 (#1280) the same value becomes the `:path` pseudo-header,\nwhere the HPACK guard drops a field carrying CR, LF or NUL -- a malformed\nrequest rather than an injection, but a silent one.\n\nModification:\nValidate on construction, following `Referer`: the value must be\nnon-empty and consist of visible ASCII only (0x21-0x7E), which is what a\nrequest target may contain on the wire (RFC 9112 section 3.2) and the\nonly thing the renderer can send faithfully. Anything else fails with an\n`IllegalArgumentException` naming the character and its index. `copy`\nand the Java `RawRequestURI.create` go through the same constructor. The\nserver-side creation under `raw-request-uri-header \u003d on` is unaffected:\nit builds the header from a target the URI parser has already accepted,\nwhich is visible ASCII by construction.\n\nDocument the constraint in the scaladoc and in the `Raw-Request-URI`\nsection of the model docs, together with the warning not to build the\nheader from unvalidated request input.\n\nResult:\nA request target that would corrupt the request line, or that the\nrenderer would corrupt, is rejected where the application creates it,\ninstead of being sent.\n\nTests:\n- `HeaderSpec` gains two cases: the full visible-ASCII range and the\n  existing `%80%fe%ff` value are accepted; space, CR+LF, LF, tab, NUL,\n  DEL, two non-ASCII characters, the empty string and a `copy` are each\n  rejected, and the message names the offending character. The rejecting\n  case fails with the validation reverted.\n- sbt \"http-core/testOnly ...HeaderSpec ...RequestRendererSpec\n  ...RequestParserCRLFSpec ...NewConnectionPoolSpec\" - 133 pass.\n- sbt \"http2-tests/testOnly ...Http2ClientSpec\" (Raw-Request-URI cases),\n  \"docs/testOnly ...ModelSpec ...ModelDocTest\" - pass.\n- sbt \"http-core/mimaReportBinaryIssues\" - pass.\n- native scalafmt clean.\n\nReferences:\nRefs #1280\n\n* Require a custom HTTP method name to be a token\n\nMotivation:\nThe request line has two application-supplied parts, and the previous\ncommit validated one of them. `HttpMethod.custom` checked only that the\nname was non-empty, and the HTTP/1.1 renderer writes it into the request\nline as given (`r ~~ method ~~ \u0027 \u0027`), so a name carrying a space, CR or\nLF ended the method early and let the rest be read as the target, the\nprotocol or a header. Over HTTP/2 the same name becomes `:method`, where\nthe HPACK guard drops a field carrying CR, LF or NUL and sends a\nmalformed request.\n\nModification:\nRequire the name to be a token (RFC 9110 section 5.6.2) in both `custom`\noverloads that construct a method, using the parser\u0027s own `tchar` class;\nthe single-argument overload and the Java `HttpMethods.custom` delegate\nto them. The server never constructs a method from wire bytes -- it looks\nthe parsed token up among the registered custom methods -- so only\napplication code is affected. Document the constraint at the custom\nmethod section of the model docs, and correct the HTTP/2 renderer\u0027s\ncomment on `Raw-Request-URI`, which still said the value was taken on\ntrust.\n\nResult:\nNeither part of the request line an application can supply can carry a\ncharacter that would corrupt it, and neither can reach the HPACK guard\u0027s\nsilent drop of a pseudo-header.\n\nTests:\n- `HttpMethodsSpec` gains two cases: every tchar and two real-world\n  custom methods are accepted; a request-line injection, CR+LF, tab, NUL,\n  DEL, `/`, `:`, a non-ASCII character, the empty string and the\n  five-argument overload are each rejected, and the message names the\n  offending character. The rejecting case fails with the validation\n  reverted.\n- sbt \"http-core/testOnly ...HttpMethodsSpec ...HeaderSpec\n  ...RequestParserCRLFSpec ...HttpHeaderSpec ...RequestRendererSpec\"\n  - 191 pass (the last three register custom methods).\n- sbt \"http-core/mimaReportBinaryIssues\" - pass.\n- native scalafmt clean.\n\nReferences:\nRefs #1280"
    },
    {
      "commit": "d9a5eb1e38c0c35a4a4d9ff80c8794f8f2ea5e5a",
      "tree": "48aa4fbb3e7be811a0f6dd5f891f176555438bc8",
      "parents": [
        "da54f5efe295e4260f2c414b0bafb8fd7e00004c"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Thu Sep 17 20:11:57 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 17 20:11:57 2026 +0100"
      },
      "message": "chore: add SPDX license headers to project-info.conf and paradox .htaccess (#1306)\n\nMotivation:\nA scan of the source tree for files without an Apache license header\nfound two that sbt-header never sees: `project/project-info.conf`,\nwhich lives outside every source set, and\n`docs/src/main/paradox/.htaccess`, which has no sbt-header file type.\nMirrors apache/pekko#3545, which added headers to the logback XML\nfiles, shell scripts and benchmark template that sbt-header misses in\npekko. pekko-http has no logback configs, its one shell script already\ncarries a header and its boilerplate templates are covered by the\n`*.scala.template` mapping in `CopyrightHeader`, so only these two\nfiles remain.\n\nModification:\nPrepend the `# SPDX-License-Identifier: Apache-2.0` line that\n`CopyrightHeader.hashLineComment` hardcodes for every hash-comment file\nin this repo, matching the header on the `reference.conf` and\n`application.conf` files and on pekko\u0027s own `project-info.conf`.\n\nResult:\nEvery tracked configuration and script file carries a license header.\nNo behavior change: HOCON and Apache httpd both treat `#` lines as\ncomments.\n\nTests:\n- `project/project-info.conf` parsed and resolved with Typesafe Config\n  1.4.6 after the change; `project-info.scaladoc` resolves as before.\n- sbt headerCheckAll - pass.\n\nReferences:\nNone - follows apache/pekko#3545"
    },
    {
      "commit": "da54f5efe295e4260f2c414b0bafb8fd7e00004c",
      "tree": "2ddadba241e9654c25f05a9a0fa39cb051e9c352",
      "parents": [
        "f46242793be76a7af2a4b6fb3a564667bf0cf6d9"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Thu Sep 17 16:23:39 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 17 16:23:39 2026 +0100"
      },
      "message": "fix: fail instead of throw when a transform on a completed FastFuture throws (#1305)\n\nMotivation:\n`FastFuture.successful` / `FastFuture.failed` return already completed futures\nthat override `Future.transform` to apply the function directly. Unlike\n`DefaultPromise`, that override did not catch exceptions thrown by the\nfunction, so the standard `filter`, `collect` and `transform` combinators on\nsuch a future threw synchronously at the call site instead of returning a\nfailed future. Callers cannot tell a FastFuture-produced `Future` from any\nother, and pekko-grpc now returns them from its public APIs.\n\nModification:\nRoute both `transform` overrides through a helper that converts a `NonFatal`\nexception from the function into an `ErrorFuture`, matching the contract of\n`Future.transform`.\n\nResult:\nStandard combinators on a completed FastFuture return a failed future when\ntheir function throws, the same as for `Future.successful` / `Future.failed`.\n\nTests:\n- sbt \"http-core/Test/testOnly org.apache.pekko.http.scaladsl.util.FastFutureSpec\"\n  - new transform/filter/collect cases fail without the fix, 32/32 pass with it\n- sbt \"http-core/mimaReportBinaryIssues\" - no issues (against 1.0.0)\n- scalafmt --mode diff-ref\u003dupstream/main\n- git diff --check\n\nReferences:\nNone - found while reviewing FastFuture stack and exception semantics for pekko-grpc"
    },
    {
      "commit": "f46242793be76a7af2a4b6fb3a564667bf0cf6d9",
      "tree": "908910cbbe1d7081eff79e5c9ae78925b3181e87",
      "parents": [
        "f019432253e6be22b19a3fde5664cc8355b35fcf"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Mon Sep 14 08:14:05 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Sep 14 08:14:05 2026 +0100"
      },
      "message": "build(deps): Bump actions/setup-java from 6.0.0 to 6.0.1 (#1304)\n\nBumps [actions/setup-java](https://github.com/actions/setup-java) from 6.0.0 to 6.0.1.\n- [Release notes](https://github.com/actions/setup-java/releases)\n- [Commits](https://github.com/actions/setup-java/compare/dd06d9cba3e5552c54d9f8ea23572deb30010f7c...de7274f081f381c8f8158605e0321c36c376e2e6)\n\n---\nupdated-dependencies:\n- dependency-name: actions/setup-java\n  dependency-version: 6.0.1\n  dependency-type: direct:production\n  update-type: version-update:semver-patch\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "f019432253e6be22b19a3fde5664cc8355b35fcf",
      "tree": "89bc86b92e3fa03d64953f6d2f3ca25ada5d300c",
      "parents": [
        "40b07a21d4f805e0662826adff21daba1b469900"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sat Sep 12 10:16:02 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Sep 12 10:16:02 2026 +0100"
      },
      "message": "chore: replace immutable.Seq with Seq, drop Scala 2.12-era imports (#1298)\n\nMotivation:\nThe codebase still carried the Scala 2.12-era `immutable.Seq` qualifier\nacross ~100 files. `scala.Seq` has been an alias for\n`scala.collection.immutable.Seq` since 2.13, so the qualifier and the\n`scala.collection.immutable` imports that only exist to support it are\nredundant. Mirrors apache/pekko#3539.\n\nModification:\n- Replace `immutable.Seq`, `collection.immutable.Seq` and\n  `scala.collection.immutable.Seq` with `Seq` in main, test, multi-jvm\n  and docs-snippet Scala sources.\n- Drop `import scala.collection.immutable` / `immutable.Seq` where no\n  other `immutable.*` member is used; keep it where TreeMap,\n  VectorBuilder, ListMap, etc. are still referenced.\n- `Http2Blueprint`: `import scala.collection.{ immutable, mutable }` -\u003e\n  `import scala.collection.mutable`.\n- Paradox docs: prose and signature mentions updated from\n  `immutable.Seq` to `Seq`; `alphabetically.md` table rows re-padded.\n- Java sources are unchanged: `scala.Seq` is a package-object alias\n  that Java cannot name.\n\nResult:\nNo remaining `immutable.Seq` idioms in Scala sources or docs. Source-\nand binary-compatible (same erasure); no MiMa filter needed.\n\nTests:\n- native scalafmt 3.7.17 on all changed Scala files\n- sbt Test/compile multi-jvm:compile (Scala 2.13.18)\n- sbt http-core/mimaReportBinaryIssues http/mimaReportBinaryIssues\n  http-cors/mimaReportBinaryIssues http-xml/mimaReportBinaryIssues\n- sbt http-cors/test; HeaderSpec, HttpHeaderSpec, FramingSpec,\n  Http2FramingSpec, HttpMessageRenderingSpec, Http*DetailedStringExampleSpec\n\nReferences:\nNone - follows apache/pekko#3539"
    },
    {
      "commit": "40b07a21d4f805e0662826adff21daba1b469900",
      "tree": "a2f77052b4c2c78dfc7764e687f4b80912737517",
      "parents": [
        "dc22225857f0cdbbe32d80b3685ea4de74764392"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 14:55:01 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 14:55:01 2026 +0100"
      },
      "message": "http/2: reject a header field carrying CR, LF or NUL, and answer a malformed field with a 400 (#1297)\n\nMotivation:\nTwo things go wrong when an HTTP/2 peer sends a malformed header field.\n\nA regular field whose value contains CR LF is silently accepted with the\nvalue truncated. `RequestParsing.parseHeaderPair` reuses the HTTP/1.1\nline parser by building `name + \": \" + value + \"\\r\\nx\"`, so the parser\nstops at the first CRLF it meets, which is now the peer\u0027s, and the header\nthat comes back is whatever preceded it. RFC 9113 section 8.2.1 says a\nfield name or value carrying NUL, CR or LF must be treated as malformed.\n\nA field the HTTP/1.1 parser does reject -- a NUL in the value, an illegal\ncharacter in the name, a value over `max-header-value-length` -- is\nreported with that parser\u0027s own, internal `ParsingException`, which is\nnot the model `ParsingException` that `HeaderDecompression` catches. It\nescapes the decompression stage and fails it, and with it the whole\nconnection and every stream on it, where the HTTP/2 engine answers other\nmalformed fields with a 400 on the one stream (#59). On a connection a\nproxy multiplexes for many users, one bad header from one of them takes\nthe connection down for all of them.\n\nModification:\nCheck the name and the value for CR, LF and NUL at the top of the HPACK\nlistener, before the field is dispatched on its name, reusing the\npredicate the rendering side uses. Neither is echoed in the error, since\neither may be what is malformed. Widen the internal `ParsingException`\nfrom `private[parsing]` to `private[http]` and rethrow it from\n`parseHeaderPair` as the model exception, so that every failure the\nHTTP/1.1 parser reports for an HTTP/2 field takes the 400 path.\n\nResult:\nA header field carrying CR, LF or NUL is answered with a 400 instead of\nbeing accepted truncated, and a field the HTTP/1.1 parser rejects is\nanswered with a 400 on its own stream instead of failing the connection.\n\nTests:\n- `RequestParsingSpec`: CR LF, bare LF and NUL in a value, CR LF in a\n  name, and a value over `max-header-value-length` each produce a\n  `BadRequest` with the expected summary.\n- `Http2ServerSpec`: the CR LF, NUL and over-long cases each get a 400 on\n  their stream and the next stream on the same connection is served.\n- With both source changes reverted all eight fail; with only the CR/LF/\n  NUL check in place the two over-long cases still fail, so the exception\n  translation is covered on its own.\n- sbt \"http2-tests/test\" - 374 pass.\n- sbt \"http-core/mimaReportBinaryIssues\" - pass.\n- native scalafmt clean.\n\nReferences:\nRefs #59"
    },
    {
      "commit": "dc22225857f0cdbbe32d80b3685ea4de74764392",
      "tree": "3fa42667286cc250885d44afe352c8724b8497e5",
      "parents": [
        "524a55751cbfb131aeb531144b0d37de406b3595"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 12:22:23 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 12:22:23 2026 +0100"
      },
      "message": "fix: start each multipart part with its own header state (#1279)\n\nWhen a body part ends without a header-separating empty line, the\n`BoundaryHeader` branch of `parseHeaderLines` emits that part and continues\ninto the next one, but passed its own `headers` and `headerCount` along. The\nboundary starts a new part, so both should start empty -- `cth` was already\nreset to `None` on the same call, which suggests the other two were simply\nmissed.\n\nTwo visible effects, both fixed here:\n\n  - the following part was reported carrying headers it never declared;\n  - `headerCount` was never reset either, so a run of such parts accumulated\n    towards `max-header-count` across parts rather than per part, failing\n    parts that are individually well within the limit.\n\nThe reset stays a direct self-call to `parseHeaderLines`: that call is what\nmakes the method `@tailrec`, and routing it through a helper would turn it\ninto an unoptimised mutual recursion.\n\nFixes #1278.\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "524a55751cbfb131aeb531144b0d37de406b3595",
      "tree": "a5ba6e6ae4de8be6497605f8e7ac33c8235ed6b8",
      "parents": [
        "7f71b136c08d76393c97de6e386400e175e6d612"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 12:16:32 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 12:16:32 2026 +0100"
      },
      "message": "hpack: decode header blocks from any InputStream, without copying (#1251)\n\n* hpack: decode header blocks from any InputStream, without copying\n\nMotivation:\nHeaderDecompression compacted every header block before decoding it, because\nthe decoder needed an InputStream with mark/reset and an available() covering\nthe whole block - which of ByteString\u0027s implementations only the array-backed\nones provide. A HEADERS frame followed by CONTINUATION frames is assembled with\n`++`, so it is a rope whose asInputStream is a SequenceInputStream, and even a\nsingle-frame payload is usually a slice of a network buffer that compact()\ncopies in full.\n\n#1231 removed the decoder\u0027s assumption that one read() fills the buffer. Three\ndependencies on ByteArrayInputStream semantics remained:\n\n- decodeULE128 used mark(5)/reset() to rewind a partially read varint\n- the main loop was driven by `while (in.available() \u003e 0)`\n- the literal name and value states waited for `available() \u003e\u003d length`\n\nModification:\nRead forward only. The main loop now ends when read() reports the end of the\nstream between representations, which is the normal end of a header block, and\nevery other state treats the end of the stream as truncated input. decodeULE128\nreads without marking, readByte reports the end of the stream as a\ndecompression failure, and skipFully skips a run in one go while coping with\nskip() returning zero. The available() guards before readStringLiteral are gone\nbecause readNBytes already reports a short literal.\n\nHeaderDecompression then hands the payload straight to the decoder, so a header\nblock is no longer copied.\n\nResult:\nNo functional change for a well-formed block. A block that ends mid\nrepresentation is now reported as a decompression failure - COMPRESSION_ERROR,\nper RFC 9113 section 4.3 - where it previously decoded to however many headers\nhad been read so far. The decoder no longer supports being fed a block\nincrementally across calls; parseAndEmit is its only caller and always passes a\ncomplete block.\n\nTests:\n- HpackDecoderSpec gains coverage over a SequenceInputStream that supports\n  neither mark/reset nor a whole-block available(), at chunk sizes 4, 3 and 1,\n  plus two truncated blocks. The chunked cases fail before this change with a\n  decompression failure - 8 passed\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.http2.*\" - 18 passed\n- sbt http2-tests/test - 352 passed, 25 ignored, 26 pending\n- scalafmtCheckAll, javafmtCheckAll, headerCheck, http-core/mimaReportBinaryIssues - clean\n\nReferences:\nFollows #1231\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n* fix: keep the HPACK decoder usable after a header fails to parse\n\nMotivation:\nHeaderDecompression\u0027s HeaderListener threw ParsingException straight through the\nHPACK decoder, which left the connection unable to decode any later HEADERS\nframe:\n\n- Decoder.insertHeader calls the listener before adding the entry to the dynamic\n  table, so the entry for the offending header was never added\n- decode() unwound at that point, so every representation after it in the block\n  was never read and never added either\n- endHeaderBlock() was called inside the try, so it was skipped and the decoder\n  kept the state and headerSize of the abandoned block\n\nThe first two desynchronise the decoder\u0027s dynamic table from the peer\u0027s\nencoder\u0027s, which HPACK cannot recover from; the third resumes the next block\npart way through a representation. HeaderDecompression answers a parse failure\nwith a bad request and keeps the connection open, so this is reachable with a\nsingle malformed header - an unknown method is enough.\n\nModification:\nCatch ParsingException in the listener, remember the first ErrorInfo and return\nnull so that decoding runs to the end of the block and the dynamic table keeps\ntracking the peer\u0027s. Report the remembered failure once the block is decoded.\nCall endHeaderBlock() in a finally as well, so anything else that unwinds - a\nmalformed pseudo header raises Http2ProtocolException - still resets the\ndecoder. The outer ParsingException handler stays as a fallback.\n\nResult:\nA request with an unparseable header still gets a bad request response, and\nsubsequent requests on the same connection are decoded correctly.\n\nTests:\n- New \"keep the connection usable after a header parsing failure\" in\n  Http2ClientServerSpec sends a request with an unknown method, expects the bad\n  request, then sends a valid request on the same connection. Without the fix\n  the second request never reaches the handler at all - the spec times out\n  waiting for it - and with the fix it is served normally\n- sbt \"http2-tests/testOnly ...Http2ClientServerSpec\" - 8 passed\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.http2.*\" - 13 passed\n- sbt http2-tests/test - 353 passed, 25 ignored, 26 pending\n- scalafmtCheckAll, headerCheck, http-core/mimaReportBinaryIssues - clean\n\nReferences:\nNoticed while working on #1251\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n---------\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "7f71b136c08d76393c97de6e386400e175e6d612",
      "tree": "6de528f6568b31db55bedf02043c86a596b12375",
      "parents": [
        "a63fd46c3b30b9e093c513a91e4f16443498c1cd"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 10:24:07 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 10:24:07 2026 +0100"
      },
      "message": "perf: avoid copying WebSocket deflate/inflate output (#1235)\n\n* perf: avoid copying WebSocket deflate/inflate output\n\nMotivation:\nPerMessageDeflate buffers the output of the Deflater/Inflater in a\nByteArrayOutputStream and then calls ByteString.fromArrayUnsafe on\ntoByteArray. ByteArrayOutputStream.toByteArray always copies the buffer,\nso every compressed or decompressed WebSocket frame pays a full copy of\nits payload.\n\nModification:\nAdd an internal ByteStringOutputStream that extends\nByteArrayOutputStream and exposes toByteStringUnsafe, wrapping the\ninternal buffer in a ByteString without copying when most of the buffer\nis used, and copying to a right-sized array otherwise so a large buffer\nis not retained by a small payload. Use it for the inflate and deflate\npaths in PerMessageDeflate, which allocate the stream per call and\ndiscard it immediately afterwards.\n\nResult:\nNo copy of the payload per WebSocket frame when permessage-deflate is\nenabled, and no oversized buffer retained when a frame only fills a\nsmall part of it.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.util.ByteStringOutputStreamSpec org.apache.pekko.http.impl.engine.ws.WebSocketServerSpec\" - 51 tests succeeded\n- scalafmt --list --mode diff-ref\u003dupstream/main - no files reported\n- sbt headerCreateAll - headers added for the new files\n\nReferences:\nNone - the ByteStringOutputStream implementation is adapted from Apache\nPekko gRPC (Apache License 2.0), https://github.com/apache/pekko-grpc/pull/862\n\n* chore: add missing MiMa filters for HttpMessageParser completion handling\n\nMotivation:\n#1246 replaced the object-level `CompletionIsMessageStartError` with a\ntrait-level `completionIsMessageStartError` in the internal\n`HttpMessageParser`, but the excludes file it added ends with the\nintroducing comment and no filter lines, so `validatePullRequest` fails\nMiMa on http-core for every PR rebased onto current main.\n\nModification:\nAdd the three `HttpMessageParser` filters under the existing comment in\n`illegal-request-context.excludes`.\n\nResult:\n`sbt \"http-core/mimaReportBinaryIssues\"` passes again."
    },
    {
      "commit": "a63fd46c3b30b9e093c513a91e4f16443498c1cd",
      "tree": "72b0dd180b379b0990ad94a0070316feafcc9904",
      "parents": [
        "7dc416d92f5a88a89b63a634dd777171ca898db3"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 10:00:58 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 10:00:58 2026 +0100"
      },
      "message": "perf: hand ByteBuffers to Inflater and CRC32 in the coding stages (#1228)\n\nMotivation:\nThree places in the gzip/deflate stages copy a ByteString into a byte array only\nto hand it to java.util.zip:\n\n- DeflateDecompressorBase.Inflate.parse calls setInput with the whole remaining\n  buffer on every parse round, via toArray, so every round copies it.\n- GzipCompressor.updateCrc and GzipDecompressor.crc16 use toArrayUnsafe, which\n  only avoids the copy for a compact ByteString and falls back to a full copy for\n  a slice or a multi-fragment one.\n\nByteReader.remainingData is input.drop(off), so it is not compact once any byte\nhas been consumed, which is always the case by the time Inflate runs. Inflater\nand Deflater have taken ByteBuffer input since Java 11 and CRC32 since Java 8,\nand the build targets JDK 17.\n\nModification:\nPass reader.remainingData.asByteBuffer to Inflater.setInput. getRemaining reports\nthe buffer\u0027s remaining bytes, so the following reader.skip is unchanged. Feed the\nchecksums one buffer per fragment with asByteBuffers, which stays copy free even\nfor a multi-fragment ByteString, where the singular asByteBuffer would compact.\n\nResult:\nNo array copy per inflate round, and no full size copy per gzipped entity.\nMeasured over a 1 MB input, 200 CRC32 updates: 221 ms via toArrayUnsafe versus\n154 ms via a ByteBuffer for a sliced input, 102 ms versus 98 ms for a compact\none, so it is never slower and stops allocating a copy of everything gzipped.\n\nTests:\n- sbt \"http-tests / Test / testOnly org.apache.pekko.http.scaladsl.coding.*\" - 78 passed. CoderSpec already covers these paths well: \u0027works for any split in prefix + suffix\u0027 decodes at every possible split point, so remainingData is a slice at every offset, plus chunked decoding and corrupt input\n- New CoderSpec case asserts the encoder produces identical output for a multi-fragment input and a compact one, pinning the per-fragment checksum. It is a regression guard, not a failing-before test, since the previous code was also correct\n- sbt http/mimaReportBinaryIssues - clean\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n\nReferences:\nNone - found while auditing ByteString usage across the code base"
    },
    {
      "commit": "7dc416d92f5a88a89b63a634dd777171ca898db3",
      "tree": "eedad9dacc1cf79fd115330e4f9677bd81098e06",
      "parents": [
        "9bbb67cb8a8b9d395308b9c54b060c8da4e15624"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 10:00:26 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 10:00:26 2026 +0100"
      },
      "message": "add warnings about limitations of idle-timeout (#1194)\n\n* add warnings about limitations of idle-timeout\n\n* Update idle connection timeout description\n\nClarified the description of the idle connection timeout to specify it as a bidirectional inactivity timeout and provided additional context on its behavior.\n\n* Enhance documentation on inactivity timeout behavior\n\nClarify the behavior of the inactivity timeout, including its bidirectional nature and implications for connection management."
    },
    {
      "commit": "9bbb67cb8a8b9d395308b9c54b060c8da4e15624",
      "tree": "5ce623e573f6dbb16711117d99540452f80ec209",
      "parents": [
        "e202ffd09197dca9c068cfae0cb02cc9f689f601"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:57:16 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:57:16 2026 +0100"
      },
      "message": "give ParsingErrorHandler access to the request that was rejected (#1245) (#1246)\n\n`ParsingErrorHandler` could not see the request it was rejecting: none of\nits four arguments describe the method or the request target, and\n`ParserOutput.MessageStartError` discarded both at the point where the\nparser gave up.\n\nAdd `IllegalRequestContext` and a five-argument `handle` overload that\nreceives it. The overload defaults to the existing four-argument method,\nso existing handlers, `DefaultParsingErrorHandler` included, keep working\nunchanged.\n\nThe parser populates the context at the failure site: its `method`,\n`uri` and `uriBytes` fields are reused across a keep-alive connection, so\nthey are cleared for every message to stop a rejection from reporting the\nprevious request\u0027s values.\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "e202ffd09197dca9c068cfae0cb02cc9f689f601",
      "tree": "d551543baf90ab4a843329b51465cc644fbd5d78",
      "parents": [
        "eed92b063f05ec6b15f053d716338234f4af1660"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:55:09 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:55:09 2026 +0100"
      },
      "message": "perf: decode parser byte ranges in bulk instead of char by char (#1229)\n\nMotivation:\nasciiString built its String one character at a time through indexed ByteString\naccess and a StringBuilder. Indexed access into a multi-fragment ByteString walks\nthe fragment list on every byte, and the loop cannot use the JDK\u0027s bulk decode.\nByteStringParserInput.sliceString already does the fast thing for the same job.\n\nIt also sign extended: input(ix).toChar turns byte 0xC3 into U+FFC3 rather than\nU+00C3. Three of the call sites can see bytes above 0x7F - the response reason\nphrase, chunk extensions, and header names when\nillegal-response-header-name-processing-mode is warn or ignore - so those ranges\nwere decoded into replacement-looking garbage. The two call sites in\nscanHeaderValue cannot: they only ever cover a range already checked to be legal\n7-bit ASCII, where the old and new behaviour agree.\n\nModification:\nasciiString now slices and calls decodeString(ISO-8859-1), matching\nByteStringParserInput.sliceString. Reuses the existing ISO88591 constant from\npekko.http.impl.util.\n\nResult:\nOne bulk decode instead of a per-byte loop, and byte ranges above 0x7F decode to\nthe Latin-1 character rather than a sign extended one. Measured over 3M\nextractions of a 4-character header name from a request-sized ByteString: 234 ms\nbefore, 167 ms after.\n\nTests:\n- sbt \"http-core / Test / testOnly org.apache.pekko.http.impl.engine.parsing.*\" - 242 passed\n- New ResponseParserSpec case parses a non-ASCII header name with illegal names ignored; it fails before this change, where the name comes out as f\\uFFC3\\uFFB6o\n- sbt http-core/mimaReportBinaryIssues - clean\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n\nReferences:\nNone - found while auditing ByteString usage across the code base"
    },
    {
      "commit": "eed92b063f05ec6b15f053d716338234f4af1660",
      "tree": "af1bbb534b6d7b9e619ac6a8d1015da3e6beb118",
      "parents": [
        "f4dbeeae6848502c4ac5015ab662a38fd1cfeaea"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:53:21 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:53:21 2026 +0100"
      },
      "message": "feat: honour Raw-Request-URI as the HTTP/2 :path (#1280)\n\nThe HTTP/1.1 request renderer lets a caller supply the request target\nverbatim through a `Raw-Request-URI` header, bypassing `Uri` rendering. The\nHTTP/2 renderer built `:path` from `request.uri` unconditionally, so the\nsame request produced a different target depending on the protocol\nnegotiated.\n\nTake the header into account when building `:path`, as HTTP/1.1 does. The\nheader is a `SyntheticHeader`, so it was already excluded from the rendered\nheader block by the `renderInRequests` filter and is only consumed here. As\nin HTTP/1.1 the value is used as given -- supplying a valid origin-form\ntarget is the caller\u0027s responsibility.\n\nThis matters because `Uri` cannot round-trip a percent-encoded path: it\ndecodes segments when parsing and re-encodes them with a keep-set that\nleaves sub-delims raw, so `%2B` renders back as `+`. Callers that must\nreproduce a target byte-for-byte -- AWS SigV4 signs the encoded path, so an\nS3 key containing `+` or `\u003d` fails with SignatureDoesNotMatch otherwise --\nhad no way to do so over HTTP/2.\n\nRefs #1273.\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "f4dbeeae6848502c4ac5015ab662a38fd1cfeaea",
      "tree": "66bbf9d1cef1b51bc8289b95d926905f80e2cd78",
      "parents": [
        "83eb9486df341dfaf22ee8d9a37148c1785cb330"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:51:29 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:51:29 2026 +0100"
      },
      "message": "Use lazy log templates instead of string interpolation (#1292)\n\nMotivation:\nSeveral main-source log calls built their message with `s\"...\"`\ninterpolation, so the message was rendered even when the level was\ndisabled. A few of them also carried redundant `if (log.isXxxEnabled)`\nguards, and one built an interpolated string that already contained\n`{}` placeholders.\n\nModification:\nConvert eager `s\"...\"` log messages to `LoggingAdapter` `{}` templates\nwith the values passed as arguments, so the message is only rendered\nwhen the level is enabled. Drop the now-redundant `isDebugEnabled` /\n`isErrorEnabled` guards in FrameOutHandler, Http2Blueprint and\nNewHostConnectionPool. Remove the unnecessary `s` prefix from the\nHttpsProxyGraphStage message, which had no interpolation at all, and\npass the slot message of NewHostConnectionPool as a template argument\nrather than splicing it into the template. Rendered messages are\nunchanged.\n\nResult:\nNo message string is built for a disabled log level, and a slot error\nmessage containing `{}` can no longer be mistaken for a placeholder.\n\nTests:\n- sbt \"http-core/compile\" \"http/compile\" - success\n- sbt \"++3.3.8\" \"http-core/compile\" \"http/compile\" - success\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.server.HttpServerSpec org.apache.pekko.http.impl.engine.ws.MessageSpec\" - 139 passed\n- sbt \"http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec\" - 174 passed\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.unmarshalling.sse.EventStreamParserOversizedSpec\" - 18 passed\n- sbt \"http-core/scalafmt\" \"http/scalafmt\" - no changes\n\nReferences:\nNone - logging cleanup"
    },
    {
      "commit": "83eb9486df341dfaf22ee8d9a37148c1785cb330",
      "tree": "0d2db753e157126741bada9c3e8167fa7dfe7147",
      "parents": [
        "9861865b37710310b1cbce45a324505e341eaf9a"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:51:03 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:51:03 2026 +0100"
      },
      "message": "fix: release buffered data accounting when an incoming HTTP/2 stream is shut down (#1281)\n\nMotivation:\n`IncomingStreamBuffer.shutdown()` fails the entity outlet and drops\nwhatever is still buffered, but unlike the other discard paths\n(`onRstStreamFrame`, `onDownstreamFinish`) it never subtracted those\nbytes from `totalBufferedData`.\n\nOne of its callers leaves the connection running: when a peer sends more\ndata than the stream-level window allows, `onDataFrame` resets that\nsingle stream with FLOW_CONTROL_ERROR and carries on. The bytes still\nbuffered for the stream, plus the payload of the frame that is dropped\nwithout ever being buffered, keep counting as buffered for the lifetime\nof the connection. The connection-level flow controller only emits a\nWINDOW_UPDATE while `outstanding + buffered` stays below half of\n`incoming-connection-level-buffer-size`, so the leaked accounting\npermanently reduces - and once it reaches half the buffer size, stops -\nthe replenishment of the connection window, and every stream on that\nconnection stalls.\n\nModification:\nRelease the buffer in `IncomingStreamBuffer.shutdown()` via the existing\n`discardBuffer()`, and subtract the payload of the frame that trips the\nstream-level window check, since that frame is never buffered either.\n\nResult:\nResetting a stream for a flow-control violation releases the\nconnection-level window its data reserved, so the connection keeps being\nreplenished and no longer stalls.\n\nTests:\n- New test \"release connection-level flow control accounting when a stream-level window is exceeded\" makes a peer overrun the stream window with the connection-level buffer sized so the discarded frame is more than half of it, then asserts the connection window is handed back in full. Without the fix no WINDOW_UPDATE is emitted at all.\n- sbt \"http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec\" - Not run - environment failure: `http2-tests / update` cannot resolve `io.github.summerwind:h2spec_darwin_amd64:2.6.0` (\"h2spec_darwin_amd64.tar.gz not found under https://github.com/summerwind/h2spec/releases/download/v2.6.0/\"), which blocks the whole module. Verified on CI instead.\n- The first CI run failed on the new test only, with \"requirement failed: incoming-connection-level-buffer-size must be \u003e 0\": the buffer size was a `val` in the anonymous setup class, which is initialised after the superclass constructor has already read `settings`. The constants now live outside that class.\n\nReferences:\nNone - releases buffered-data accounting when the incoming side of a stream is shut down"
    },
    {
      "commit": "9861865b37710310b1cbce45a324505e341eaf9a",
      "tree": "e6c549ba9029a05fa1ed848dee1b6a7386564ad9",
      "parents": [
        "5b9a02ee7b30a391aa941f7563e3cc389530a0a6"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:43:29 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:43:29 2026 +0100"
      },
      "message": "fix: cancel the pending slot state timeout when a pool slot is shut down (#1283)\n\nMotivation:\nEvery state transition in `NewHostConnectionPool`\u0027s slot state machine\ncancels the timeout of the state it leaves and, if the new state defines\none, schedules a fresh `materializer.scheduleOnce` task for it. Those\ntasks are not bound to the stage lifecycle, so they keep running after\nthe stage has stopped.\n\n`Slot.shutdown()`, which `postStop` calls for every slot, closed the\nconnection and ran `state.onShutdown` but never cancelled that timeout.\nA pool that stops while a slot is in a state with a finite timeout -\nwaiting for a connection, for a response entity subscription, or for the\nkeep-alive timeout - therefore leaves a scheduled task behind that keeps\nthe slot, and through it the whole pool logic with its connections and\nrequest contexts, reachable until the timeout elapses. Firing does\nnothing useful either: the task only invokes an async callback on a\nstage that is already gone.\n\nModification:\nCancel the current timeout at the start of `Slot.shutdown()`, like every\nstate transition and the slot error path already do.\n\nResult:\nShutting a pool down no longer leaves scheduled state timeouts, and the\npool becomes collectable as soon as it stops.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.client.pool.NewHostConnectionPoolSpec\" - pass (1 test). Reverting only the `cancelCurrentTimeout()` call makes it fail, as the scheduled task is still live after the pool stopped.\n- sbt \"http-core/mimaReportBinaryIssues\" - pass.\n- scalafmt --mode diff-ref\u003dupstream/main --test - pass.\n\nReferences:\nNone - found while auditing `src/main` for resource leaks"
    },
    {
      "commit": "5b9a02ee7b30a391aa941f7563e3cc389530a0a6",
      "tree": "26a0848722f504f469e9e222743319204cc93ded",
      "parents": [
        "20fb7386e12a988e916b78df29ee2699377a44b2"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:43:10 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:43:10 2026 +0100"
      },
      "message": "fix: cancel open request timeouts when the server connection stage stops (#1284)\n\nMotivation:\n`RequestTimeoutSupport` schedules a request\u0027s timeout through\n`materializer.scheduleOnce` and keeps the `TimeoutAccess` in\n`openTimeouts` until the response for that request comes back, which is\nwhen the timeout is cancelled.\n\nThe stage had no `postStop`, so a connection that goes away with\nrequests still open - the client disconnecting, the connection being\naborted, or the server being torn down - left one scheduled task per\nopen request behind. Those tasks keep the request, the timeout handler\nand the stage\u0027s async callback reachable for as long as the configured\n`request-timeout` (20 seconds by default), and firing them achieves\nnothing: they only invoke a callback on a stage that is already gone.\n\nModification:\nCancel everything still in `openTimeouts` in `postStop`, using the same\nbest-effort `clear()` the regular response path uses.\n\nResult:\nConnections that end before their requests are answered no longer leave\nscheduled timeouts behind.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.server.HttpServerSpec org.apache.pekko.http.impl.engine.server.HttpServerWithExplicitSchedulerSpec\" - pass (66 tests). Reverting only the `postStop` addition makes the new test fail, as the timeout still fires after the stage stopped.\n- sbt \"http-core/mimaReportBinaryIssues\" - pass.\n- scalafmt --mode diff-ref\u003dupstream/main --test - pass.\n\nReferences:\nNone - found while auditing `src/main` for resource leaks"
    },
    {
      "commit": "20fb7386e12a988e916b78df29ee2699377a44b2",
      "tree": "47f6385e1022718498fbb2c435392536afebb5f4",
      "parents": [
        "a35e11fc7d88767c6f3b5a94aeaa1f16b6717e7c"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:41:47 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:41:47 2026 +0100"
      },
      "message": "fix: fail requests still queued in a pool interface that stops (#1282)\n\nMotivation:\n`PoolInterface.Logic` keeps requests that the pool cannot take yet in an\n`ArrayDeque` of `RequestContext`s, each holding the promise that the\ncaller of `Http().singleRequest` (or of a cached host connection pool\nflow) is waiting on.\n\nWhen the stage stops without having dispatched them - the pool flow\nfailing or cancelling, or the materializer shutting down - `postStop`\nonly failed `shutdownPromise`. Nothing else holds those queued contexts:\nthey were never handed to a connection, so no response, no error and no\nretry through the pool master can ever reach them, and the callers\u0027\n`Future[HttpResponse]`s stay uncompleted for the lifetime of the\nprocess.\n\nModification:\nDrain the buffer in `postStop` and fail every queued response promise\nwith the same `IllegalStateException` that is used for the shutdown\npromise.\n\nResult:\nA pool that stops unexpectedly fails the requests it never dispatched\ninstead of leaving their callers waiting forever.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.client.PoolInterfaceSpec\" - pass (1 test). Reverting only the `postStop` change makes it fail, as the queued promises are never completed.\n- sbt \"http-core/mimaReportBinaryIssues\" - pass.\n- scalafmt --mode diff-ref\u003dupstream/main --test - pass.\n\nReferences:\nNone - found while auditing `src/main` for resource leaks"
    },
    {
      "commit": "a35e11fc7d88767c6f3b5a94aeaa1f16b6717e7c",
      "tree": "a4624d7916f453101d8acffd847e31f442b2879f",
      "parents": [
        "ff9f88e1a8dcb715798d32e955a72f5cf4e9537f"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Sep 11 09:37:59 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 11 09:37:59 2026 +0100"
      },
      "message": "fix: filter the ParserSettingsImpl accessors shifted by max-part-count (#1293)\n\nMotivation:\n`maxPartCount` was added as the 12th field of `ParserSettingsImpl`, which\nshifted every positional case class accessor after it by one. MiMa checks\n`pekko-http-core` against 1.0.0, and the existing filters covered the\npositions that had already moved (12-21, 24, 26) but not 22 and 25, whose\nresult types now differ from 1.0.0. `Check / Binary Compatibility` and\n`validatePullRequest` therefore fail on main for every PR: Scala 2.13\nreports `copy$default$22` and `copy$default$25`, and Scala 3 additionally\nreports the `_22` and `_25` accessors.\n\nModification:\nAdd the four missing `IncompatibleResultTypeProblem` filters to\n`max-part-count.excludes`, the file introduced by the change that caused\nthe shift, with a comment recording why those two positions moved.\n\nResult:\n`http-core/mimaReportBinaryIssues` passes again on Scala 2.13 and Scala 3,\nunblocking MiMa validation for open PRs. `ParserSettingsImpl` is\n`@InternalApi private[pekko]`, so the shifted synthetic accessors are not\npart of the public API.\n\nTests:\n- sbt \"http-core/mimaReportBinaryIssues\" - success (Scala 2.13.18)\n- sbt \"++3.3.8\" \"http-core/mimaReportBinaryIssues\" - success (Scala 3.3.8)\nNo test sources changed - this is a MiMa filter-only change.\n\nReferences:\n- https://github.com/apache/pekko-http/actions/runs/34544677238/job/103094612413\n- Shift introduced by #1266 (feat: bound the number of parts in a multipart entity)"
    },
    {
      "commit": "ff9f88e1a8dcb715798d32e955a72f5cf4e9537f",
      "tree": "5257e4d43bd7c749aafbe15bf3325d975a4c2c71",
      "parents": [
        "7e39060b6643b1a0490fd34cf22de7a5c99f5dd2"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Thu Sep 10 21:49:43 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 21:49:43 2026 +0100"
      },
      "message": "feat: bound the number of parts in a multipart entity (#1266)\n\n* feat: bound the number of parts in a multipart entity\n\nMotivation:\n`BodyPartParser` had no limit on how many body parts one multipart\nentity may contain. Each part costs a set of parsed headers and an\nentity of its own, so a body packed with minimal parts - a boundary, a\nshort Content-Disposition and an empty body, on the order of 40 to 60\nbytes each - amplifies the work and allocation a request of a given\nsize causes. A body at the default `max-content-length` of 8m runs to\nwell over a hundred thousand parts, all of which the strict and form\nfield paths materialise. `max-content-length` bounds the bytes but not\nthat amplification, and peers such as Commons FileUpload and Spring\ngrew an explicit part limit for the same reason.\n\nModification:\nAdd a `max-part-count` parser setting, enforced where the parser\nstarts the headers of a new part, and fail the entity once it is\nexceeded. The three call sites that begin a part now go through\n`parsePartHeaderLines`; the recursive calls that continue the headers\nof the current part are unaffected, as is the closing boundary, which\ndoes not start a part.\n\nThe default of 10000 is deliberately generous. It is chosen to stay\nabove the largest part count the test suite exercises, an existing\ncase that parses 5000 parts in one go, rather than to be the tightest\nuseful bound - it still cuts the worst case by more than an order of\nmagnitude, and an application that knows its forms are small can set\nit far lower. A tighter default would be defensible if the 5000 part\ncase is not a capability worth keeping.\n\nResult:\nA multipart body can no longer be packed with an unbounded number of\nparts; the count is bounded by configuration.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.unmarshalling.*MultipartUnmarshallersSpec* org.apache.pekko.http.scaladsl.marshalling.MarshallingSpec org.apache.pekko.http.scaladsl.server.directives.FileUploadDirectivesSpec\" - pass (95 tests), including the existing \"many small parts received in one go\" case. A new test configures a limit of 2, sends 3 parts and expects the entity to fail; verified it fails with the limit check disabled, in both the CRLF and LF variants.\n- sbt http-core/mimaReportBinaryIssues, sbt http/mimaReportBinaryIssues - pass, with an exclude for the new method on the internal BodyPartParser.Settings\n- sbt \"+http-core/compile\" - pass on 2.13.18 and 3.3.8\n\nReferences:\nNone - bounds the number of parts in a multipart entity\n\n* Add the Scala 3 MiMa excludes for the new ParserSettings members\n\nThe excludes for the two ParserSettings members are only needed on\nScala 3; the 2.13 check filters them under a broader rule, so a scoped\nrun on the default Scala version alone did not surface them.\n\n* fix: count empty parts towards max-part-count\n\nA run of consecutive boundaries, with no headers or body between them,\nstarts each of its parts in the `BoundaryHeader` branch of\n`parseHeaderLines`, which recursed into `parseHeaderLines` directly and so\nnever went through `parsePartHeaderLines`. `partCount` was incremented once\nfor the whole run and the configured limit went unenforced -- for the very\nshape that amplifies hardest, since an empty part costs only a boundary and\nan end-of-line.\n\nCount the part where that branch begins a new one instead. The counting is\nsplit out of `parsePartHeaderLines` into `startPart`/`failMaxPartCount` so\nthe branch can keep its self-recursive call to `parseHeaderLines`: that call\nis what makes the method `@tailrec`, and routing it through\n`parsePartHeaderLines` would turn it into an unoptimised mutual recursion of\nup to `max-part-count` frames -- the stack overflow the trampoline in\n`parseEntity` already guards against.\n\nReported by samueleresca in review of #1266.\n\nCo-Authored-By: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e\n\n---------\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "7e39060b6643b1a0490fd34cf22de7a5c99f5dd2",
      "tree": "9fb29134f91106db8148bc7a266bcf1406e915bc",
      "parents": [
        "c428fede4a625a68bdb6d9fa7a989f0a526ef06d"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Thu Sep 10 09:39:57 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 09:39:57 2026 +0100"
      },
      "message": "fix: reject whitespace between chunk-size digits (#1257)\n\nMotivation:\nThe chunk-size parser tolerates whitespace after the size digits\n(illegal per the spec but seen in the wild, see #1812), but the `WSP`\narm recursed with the same accumulated size and then let parsing\ncontinue with the next character — including a further hex digit. So a\nchunk size such as `5 0` was read as `0x50` \u003d 80 bytes, while a proxy\nthat stops at the space reads 5. That discrepancy is a request-\nsmuggling primitive when Pekko HTTP sits behind such a proxy. The\ncomment referencing #1812 intended only trailing whitespace; the\nimplementation skipped interior whitespace too. The `WSP` arm also had\nno `cursor \u003e offset` guard, so a size field of only spaces parsed as 0.\n\nModification:\nThread a `sawWhitespace` flag through `parseSize`. Once whitespace has\nbeen seen, a hex digit no longer matches the accumulation arm and falls\nthrough to the illegal-character case, so only more whitespace or a\nterminator may follow. Guard the `WSP` arm with `cursor \u003e offset` so\nleading whitespace (before any digit) is rejected as well.\n\nResult:\nTrailing whitespace after the chunk size is still tolerated, but\nwhitespace between size digits, and leading whitespace, are rejected\nwith \"Illegal character \u0027...\u0027 in chunk start\" — Pekko HTTP and a\nfronting proxy can no longer disagree on the chunk size.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.parsing.RequestParserCRLFSpec org.apache.pekko.http.impl.engine.parsing.RequestParserLFSpec\" - pass (116 tests); two new tests reject `5 0` (interior whitespace) and ` 5` (leading whitespace), and the existing \"incorrect but harmless whitespace after chunk size\" test still passes. Verified the interior-whitespace test fails with the fix stashed (`5 0` parses as 80 bytes).\n- sbt http-core/mimaReportBinaryIssues - pass (internal impl.engine.parsing change, no public API).\n\nReferences:\nRefs #1812 - narrows the whitespace tolerance to trailing whitespace only"
    },
    {
      "commit": "c428fede4a625a68bdb6d9fa7a989f0a526ef06d",
      "tree": "e19c402b2cb8edc43061501df138b5556ae35c8e",
      "parents": [
        "e2a096de91a479efdaccb5a8a883ff6b09368689"
      ],
      "author": {
        "name": "Jarek Potiuk",
        "email": "jarek@potiuk.com",
        "time": "Thu Sep 10 09:48:01 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 08:48:01 2026 +0100"
      },
      "message": "Add a draft threat model and wire it for discoverability (#1242)\n\n* Add a draft threat model and wire it for discoverability\n\n### Motivation\nThe Pekko PMC asked the ASF Security team for a draft threat model to refine,\nas preparation for a Claude security scan. This is the companion to the same\nchange in apache/pekko.\n\ndocs/src/main/paradox/security.md already has a \"Security model\" section, and\nit makes the most important statement in this document: applications should not\nbe exposed to the public internet directly, and DoS resistance is claimed only\nas \"pretty well under most known\" attacks. But that is three sentences, and the\nquantitative envelope that actually bounds untrusted input -- the parsing limits\nin http-core\u0027s reference.conf -- is not connected to it anywhere.\n\nSeparately, the AGENTS.md -\u003e SECURITY.md chain a scanning agent follows to\nlocate a project\u0027s model does not resolve: there is no SECURITY.md in the repo.\n\n### Modification\n- THREAT_MODEL.md: a v0 draft following the Scovetta rubric. §5a collects the\n  shipped parsing limits, timeouts and CORS defaults as the model\u0027s quantitative\n  spine; §8 states the six default-on properties they underwrite.\n- SECURITY.md: reporting policy pointing at security@apache.org, plus the three\n  points that catch most reporters.\n- AGENTS.md: a Security section pointing at both.\n\nNothing security.md asserts has been dropped or weakened; §15 back-maps each\nexisting statement to the section that now carries it.\n\n### Result\nThe chain AGENTS.md -\u003e SECURITY.md -\u003e THREAT_MODEL.md resolves on main, and\ntriagers have a citable document with a closed set of dispositions (§13).\n\n§14 holds 10 questions, each stated as a proposed answer. The two worth reading\nfirst are Q1 (where exactly is the DoS line, given security.md\u0027s deliberately\ngraded claim?) and Q2 (http-cors ships allowed-origins \u003d \"*\" with\nallow-credentials \u003d yes, which per its own reference.conf echoes the request\nOrigin -- deliberate, or should the default change?).\n\nThe document is not ready to be treated as canonical until those are answered.\n\n### Tests\nNot run - docs only\n\n### References\nNone - requested by the Pekko PMC chair on the ASF security scan thread\n\n* Update THREAT_MODEL.md\n\n* Answer Q1 and Q2, and record the defaults posture as §5b\n\nMotivation:\nThe draft threat model left ten questions open in §14. Two of them were not\nlookups but rulings only the PMC could make, and until they were settled a\ntriager had no line to apply: Q1, the boundary of the \"behaves pretty well\nunder most known DoS attacks\" claim, and Q2, the CORS defaults. Separately,\nthe model had no statement of the project\u0027s standing position on configuration\ndefaults, so every \"this default should be stricter\" report had to be argued\nfrom scratch.\n\nModification:\n- §14 Q1 answered: the DoS line is content vs. volume. A single request within\n  every §5a limit that provokes super-linear CPU or memory is VALID; a finding\n  needing a limit raised is OUT-OF-MODEL: non-default-build; one depending on\n  request volume is BY-DESIGN: property-disclaimed. Propagated to §4, §5, §5a,\n  §7, §8, §9, §11a and §13.\n- New §5b, \"Security posture: hardening, not secure-by-default\", mirroring\n  apache/pekko#3478 §5b: defaults are compatibility choices, a request to\n  tighten one is a change request not a vulnerability, proposals belong on the\n  development list, and an implementation that does not do what it documents is\n  a defect this project fixes.\n- §14 Q2 answered from §5b: the CORS pair is a compatibility default, inherited\n  with the code donated by Lomig Mégard (legal/CorsNotice.txt). A request to\n  change it is BY-DESIGN: default-configuration; a cors() directive that admits\n  an origin its configuration should have rejected remains in scope.\n- New §13 disposition BY-DESIGN: default-configuration, disambiguated from\n  OUT-OF-MODEL: non-default-build (a real defect reachable only off-default,\n  versus no defect at all).\n- SECURITY.md states both rulings for reporters, and closes on what the project\n  does want: an implementation that does not match its documentation.\n\nResult:\nResource exhaustion and default-configuration reports now route to exactly one\n§13 disposition instead of being judgment calls. Four of the ten §14 questions\nare closed; the six remaining are confirm-or-correct. Provenance moves from\n17 documented / 0 maintainer / 15 inferred to 17 / 6 / 13.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242\n\n* Answer Q4-Q8, resolve Q9 against the source, correct the client-IP claim\n\nMotivation:\nSix of the ten §14 questions were still open, and two of them rested on\nstatements that the source contradicts. Q4 asserted that Pekko HTTP \"neither\nparses nor trusts forwarding headers\", which is not what extractClientIP does.\nQ9 held §5\u0027s negative claims as the document\u0027s last uncited assertions, and the\nequivalent section in apache/pekko#3478 turned out to be wrong on review, so\nasserting rather than checking them was not good enough here either.\n\nModification:\n- Q4 answered, and its stated basis corrected: extractClientIP resolves\n  X-Forwarded-For (first address) -\u003e X-Real-Ip -\u003e the remoteAddress attribute\n  (MiscDirectives.scala:142-145), so forwarding headers are parsed, just never\n  implicitly. The spoofability is documented at the directive and answered by\n  extractDirectClientIP (#1219), which reads the attribute alone. Disposition is\n  unchanged: BY-DESIGN: property-disclaimed, with a defect in\n  extractDirectClientIP being VALID. Propagated to §5, §6, §9, §10.5, §11, §11a.\n- Q5, Q6, Q7 and Q8 answered as proposed: the verify/provideVerify split, the §2\n  module in/out split, the non-goals and adversary split, and TLS cipher\n  selection as a deployment property. Q8 additionally records that in-process\n  termination is supported while the fronted posture is what the docs recommend.\n- Q9 resolved by scanning the main sources of http-core, http, parsing,\n  http-caching and http-cors: no addShutdownHook, ProcessBuilder/Runtime.exec,\n  Signal/SignalHandler, file-writing API, System.setProperty/Security.*, or bind\n  outside the public Http().bind* entry points. §5 now cites the scan, and\n  records the inherited caveat that the ActorSystem registers shutdown hooks\n  Pekko HTTP does not.\n\nResult:\nNo claim in the document is inferred any more; provenance moves from\n17 documented / 6 maintainer / 13 inferred to 20 / 24 / 0. Q10 (coexistence with\nsecurity.md) is the only question left open.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242\n\n* Answer Q9: Pekko HTTP registers no shutdown hook of its own\n\nMotivation:\nQ9 was resolved against the source but left the disposition open: §5\u0027s scan\nshowed Pekko HTTP adds no JVM shutdown hook, while the ActorSystem it runs on\ndoes. Whether to state that boundary here or defer the topic to the companion\nmodel needed a ruling, and the distinction is easy to misattribute in triage.\n\nModification:\n- Q9 answered: highlight the ActorSystem\u0027s hooks in §5 so integrators are not\n  surprised, but the claim this document makes is that Pekko HTTP registers\n  none of its own.\n- Promoted \"Registers no JVM shutdown hook\" from a trailing clause on the\n  signal-handler bullet to its own §5 claim, so the load-bearing statement is\n  stated directly rather than implied.\n- Sharpened the §5 caveat to separate the two readings: \"a Pekko HTTP process\n  has no shutdown hook\" is false, \"Pekko HTTP registers no shutdown hook\" is\n  true and is what this section asserts.\n- New §11a non-finding: a report that the process registers shutdown hooks is\n  OUT-OF-MODEL: unsupported-component, since the hooks are CoordinatedShutdown\u0027s\n  and, with remoting enabled, Artery\u0027s.\n\nResult:\nQ10 (coexistence with security.md) is the only question left open. Provenance is\n20 documented / 26 maintainer / 0 inferred.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242\n\n* Answer Q10: three documents, each canonical for one thing\n\nMotivation:\nQ10 was the last open question: which document is canonical for what, now that\nTHREAT_MODEL.md, SECURITY.md and docs/src/main/paradox/security.md all carry\nsecurity information. apache/pekko#3478 settled the same question for the\ncompanion project, and following its split keeps the two models consistent for\nanyone triaging across both.\n\nModification:\n- Q10 answered with the three-document table from apache/pekko#3478: SECURITY.md\n  is canonical for the reporting policy, THREAT_MODEL.md for scope and triage,\n  and docs/src/main/paradox/security.md for announcements and the docs-site\n  index. Every other document links rather than restates.\n- docs/src/main/paradox/security.md gains links to SECURITY.md and\n  THREAT_MODEL.md, and its reporting paragraph now names security@apache.org\n  rather than \"our private security mailing list\", which does not exist. This\n  mirrors the equivalent change in apache/pekko#3478.\n- Corrected the draft\u0027s own proposal, which suggested reducing the \"Security\n  model\" section of security.md to a pointer. It is kept: §4 quotes it as the\n  documented source of the \"should not be exposed to the public internet\n  directly\" posture and §15 back-maps four claims to it, so reducing it would\n  delete the evidence this model is built on.\n\nResult:\nAll ten §14 questions are answered. One item is referred rather than settled, as\nthe ruling requires: security.md carries an upstream-coordination sentence about\nsharing reports with the Lightbend Akka team that SECURITY.md does not, and a\nreporting statement is promoted verbatim or dropped by maintainer decision, never\nsilently moved. Provenance is 20 documented / 27 maintainer / 0 inferred.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242\n\n* Correct two §5/§9 claims against the source and answer Q3\n\nMotivation:\nMaintainer review found two claims that do not hold on main and one\ninternal inconsistency:\n- §5 claimed Pekko HTTP writes no files, but fileUploadAll creates temp\n  files and storeUploadedFile(s) writes entity bytes to an\n  application-chosen destination (FileUploadDirectives.scala:178).\n- §9 and §14 Q3 described safeDirectoryChildPath as containing\n  traversal, but its canonical-path check compares strings, so a\n  symlink resolving into a sibling directory that shares the served\n  root as a string prefix escapes it (fix in flight in #1218).\n- The status line said both \"Q1-Q8 answered\" and \"all ten answered\"\n  while Q3 still ended in an open question, and the pinned commit was\n  the PR\u0027s own first commit rather than the main commit reviewed.\n\nModification:\nRestate the §5 file-system claim with the upload-directive carve-out\nand correct Q9 accordingly. Answer Q3 as a correction: the escape is\nVALID under §5b.4 and fixed by #1218; update §9 and the §15 back-map to\nmatch. Note in §12 that #1217 would invalidate the shutdown-hook claim\non merge. Fix the status line, the tag tally, and the commit pin\n(444d939 -\u003e 85d7243, the main commit the branch is based on).\n\nResult:\nEvery §5 negative claim matches the source at the pinned commit, Q3 is\nanswered consistently with the \"all ten answered\" status, and the two\nin-flight PRs that touch the model\u0027s claims (#1217, #1218) are\ncross-linked.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1218, Refs #1217\n\n* Update the §12 note on #1217: reworked to a CoordinatedShutdown task\n\nMotivation:\n§12 flagged #1217\u0027s raw JVM shutdown hook as invalidating the §5\n\"registers no shutdown hook\" claim on merge. That PR has since been\nreworked to register its temp-file cleanup as a CoordinatedShutdown\ntask on the actor system instead, so no §5 claim is affected.\n\nModification:\nRestate the §12 bullet as a recorded near-miss rather than a pending\ninvalidation.\n\nResult:\n§12 matches the current state of #1217 and the §5/§11a claims stand.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1217\n\n* Fold the reworked #1217 and #1218 into §5, §9 and Q3\n\nMotivation:\nBoth in-flight PRs the model references changed shape after review.\n#1218 gained a documented platform caveat - File.getCanonicalPath does\nnot resolve NTFS symbolic links or junctions on Windows, so the\nlink-escape class stays open there - and now rejects path segments that\nno file-system path may contain instead of erroring. #1217 replaced the\nper-file deleteOnExit with one temp directory per actor system removed\nby a CoordinatedShutdown task. Q3\u0027s answer claimed symlink escapes are\nrejected \"whatever its target is named\", which overclaims on Windows.\n\nModification:\nScope the Q3 and §9 symlink-rejection claims to platforms where\ncanonicalization resolves links, record the Windows residual and the\ntoRealPath follow-up, note the invalid-segment hardening, and update\nthe §5 upload-directive note to describe #1217\u0027s per-system directory\nand CoordinatedShutdown cleanup.\n\nResult:\nThe model\u0027s containment and file-writing claims match what #1217 and\n#1218 actually implement, on every platform they address.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1217, Refs #1218\n\n* Record the decoding limits that bound decompression bombs\n\nMotivation:\nReview of the threat model pointed out that §9 lists decompression bombs\nas an attack class \"left to the caller\", which is not accurate: the\nrouting block ships two limits that bound a decoded entity, and the\ndecoding directives apply them without the caller doing anything.\n\nModification:\nAdd `routing.decode-max-bytes-per-chunk` and `routing.decode-max-size` to\nthe §5a limits table, and amend its intro and citation to cover the\nsecond reference.conf they live in. Rewrite the §9 bullet to state what\nthe directives bound, cited to CodingDirectives.scala:93-102, and narrow\nwhat remains the caller\u0027s: decoding outside the directives, and raising\ndecode-max-size to infinite.\n\nResult:\n§5a lists every shipped limit that bounds an entity, decompression\nincluded, and §9 no longer disclaims a control the project provides.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242\n\n* Re-pin the model to the rebased base and correct P2 against the render path\n\nMotivation:\nRebasing the branch moved its base from `85d7243` to `6740cbd`, twenty-four\ncommits later, so the document\u0027s \"written against\" pin no longer named a\ncommit on the branch. Several of those commits touched claims the document\nmakes, and one of them falsified a §8 property as stated.\n\nP2 (\"response splitting is blocked\") was asserted from the two\n`illegal-response-header-*-processing-mode` defaults alone, without\nwalking the path those settings govern. Walking it finds the guard lived\nin a single `~~(HttpHeader)` overload, so chunked-response trailers and\nchunk extensions rendered around it (#1256), it tested CR and LF but not\nNUL (#1260), and the HTTP/2 header path had no equivalent check, so the\nmitigation an application relies on under HTTP/1.1 disappeared on upgrade\n(#1258). All three are fixed at the new base. #1217 and #1218, which the\ndocument tracked as in flight, have also landed.\n\nModification:\nRe-pin to `6740cbd`. Restate P2 as a claim about the merged enforcement\npath — naming the trailer, chunk-extension, NUL and HTTP/2 gaps and the\nsingle `Rendering.isIllegalHeaderChar` predicate that now holds the rule\n— and record how it came to be stated that precisely. Update §5 and §12\nfor #1217 merging in its reworked `CoordinatedShutdown` form, re-checked\nagainst the merged code: no `addShutdownHook` outside a comment saying\nwhy one is not used. Make §5b.4\u0027s three examples concrete, two of them\nhaving stopped being hypothetical during review. Add to the §5a CORS\nruling that it governs the default and never the enforcement, citing\n#1262, where `validateOrigins` accepted a request if any origin matched\nwhile the response echoed every origin it was given.\n\nResult:\nThe pin names the commit the claims are verified against, P2 describes\nwhat the code does rather than what two settings imply, and the CORS\nsection distinguishes a permissive default from a restrictive setting\nthat fails to restrict.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242, #1256, #1258, #1260, #1262, #1217, #1218\n\n* Account for the rest of the fixes merged since the pin, and fix four citations\n\nMotivation:\nRe-pinning caught #1217, #1218 and the three response-splitting fixes,\nbut six more merged between `85d7243` and `6740cbd` and the document\naccounts for none of them. Two matter: P1 was asserted the same way P2\nwas — from the §5a table, which records what each limit is meant to\nbound rather than what the code does when one is reached — and three of\nthose bounds did not hold. Separately, four source citations no longer\npoint at the code they name.\n\nModification:\nRestate P1 as a claim about enforcement and add the note recording how it\ncame to be stated that way: `max-chunk-count` reported the limit without\nstopping the parse (#1220), `max-header-count` skipped the branch that\nmerges repeated Connection headers (#1255), and HTTP/2 connection-level\nbuffer accounting never decremented on a discarded buffer, stalling every\nstream on the connection once the leak passed half the configured size\n(#1259). Give the §9 smuggling bullet the framing decision Pekko HTTP\ndoes own, citing #1267. Add the three HTTP/2 buffer bounds to §5a and to\nthe §15 back-map, together with the decoding limits.\n\nCitations: `Http.scala` no longer has public `bind`/`bindAndHandle`\nmethods, so §5 now names `newServerAt` and the `ServerBuilder` it\nreturns; `FileAndResourceDirectives.scala:229-274` moved to `246-296`;\n`CorsSettingsImpl.scala:64` is `63`. §2 listed `http2-tests` as the\nin-model HTTP/2 module while the same table marks it out of model — the\nengine is `http-core`.\n\nResult:\nEvery fix merged since the pinned commit is either reflected in a claim\nor deliberately not: #1239 (no body on an HTTP/2 HEAD response) and #1261\n(ignoring the reserved bit of a stream identifier) are conformance fixes\nthat bear on no claim the document makes. Provenance is 20 documented /\n31 maintainer / 0 inferred.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242, #1220, #1255, #1259, #1267\n\n* Close three gaps the open PRs exposed in the model\n\nMotivation:\nReviewing the open PRs from the last three weeks against the document\nturned up three claims that are wrong or missing today, independently of\nwhether any of those PRs merge.\n\nModification:\nP1 claims inbound messages are bounded by the §5a limits, and for HTTP/2\nframes nothing bounds them: the frame parser reads the 24-bit length\nfield and takes that many bytes, up to 16 MiB - 1, before HPACK decoding\nand before the entity is assembled, so none of the three HTTP/2 buffer\nbounds apply. State the gap under P1, name #1264 as the fix in flight,\nand rule an oversized-frame report VALID against P1 rather than a §12\nmodel gap in the meantime.\n\nThe `frame-type-throttle` block was absent from the whole document, even\nthough it ships on by default charging RST_STREAM at 100/s and is the\nproject\u0027s HTTP/2 Rapid Reset mitigation (CVE-2023-44487). Add it to §5a\nand to the §15 back-map, and restate P6: `max-concurrent-streams` alone\nbounds concurrency and not work, because a reset frees the slot at once.\n\n§9 claimed traversal containment for \"file-serving directives\" without\nqualification. Three tiers, checked against the source: `getFromDirectory`\nand `listDirectoryContents` (and the browseable pair built on them) get\n`safeDirectoryChildPath`, segment filter plus canonical containment;\n`getFromResourceDirectory` gets `safeJoinPaths` alone, coherent since a\nclass-loader resource name has no canonical form; `getFromFile` and\n`getFromResource` get neither. Scope §14 Q3\u0027s answer to the first tier\nand name `getFromResource` in the §11 misuse list, where only\n`getFromFile` was listed.\n\nResult:\nThe containment claim matches what each directive implements, the Rapid\nReset mitigation is claimed where a reader of §8 will find it, and the\none bound P1 asserts but does not yet have is written down. Provenance is\n20 documented / 32 maintainer / 0 inferred.\n\nTests:\nNot run - docs only\n\nReferences:\nRefs #1242, #1264, #1263\n\n---------\n\nCo-authored-by: PJ Fanning \u003cpjfanning@users.noreply.github.com\u003e"
    },
    {
      "commit": "e2a096de91a479efdaccb5a8a883ff6b09368689",
      "tree": "5261ae5f7971b50331894f32bc51ae1b013dc1a2",
      "parents": [
        "6740cbd3a3c6b5f6a0b66e62de962c83e4529db9"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Thu Sep 10 07:10:42 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Sep 10 08:10:42 2026 +0200"
      },
      "message": "Fix typo in reference.conf comment (#1289)"
    },
    {
      "commit": "6740cbd3a3c6b5f6a0b66e62de962c83e4529db9",
      "tree": "bb47e86dea2bbb4dd6270e65f66bf8af6cc0670e",
      "parents": [
        "2c5b6f7726d1a424ba7a4bf35368f8341bdda6cc"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 08 23:15:03 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 22:15:03 2026 +0100"
      },
      "message": "Update sbt-mima-plugin to 1.2.0 (#1286)\n\n* Update sbt-mima-plugin to 1.2.0\n\n* Add MiMa excludes for the additional checks in mima 1.2.0\n\nMotivation:\nsbt-mima-plugin 1.2.0 inspects members that 1.1.6 skipped - static\nforwarders, synthetic case-class members (copy, copy$default$N, apply,\nunapply, _N accessors), trait-field accessors and deprecated members.\nAgainst pekko-http-core 1.0.0 and pekko-http 1.0.0 this surfaces 79 and\n11 additional problems on Scala 2.13 (45 and 7 on Scala 3), failing the\nCompile and test jobs.\n\nModification:\nAdd a MiMa exclude file to http-core and to http covering the newly\nreported problems. Every excluded member is internal API: the\norg.apache.pekko.http.impl packages, or the @InternalApi @DoNotInherit\nprivate[http] HTTP/2 settings traits and their Impl case classes. No\npublic API is excluded.\n\nResult:\nMiMa passes for http-core and http on both published Scala versions with\nthe 1.2.0 plugin, and the extra detections stay enabled for future\nchanges.\n\nTests:\n- sbt \"http-core/mimaReportBinaryIssues\" \"http/mimaReportBinaryIssues\" -\u003e success (Scala 2.13.18)\n- sbt \"++3.3.8!\" \"http-core/mimaReportBinaryIssues\" \"http/mimaReportBinaryIssues\" -\u003e success (Scala 3.3.8)\n- No tests added: build-only change with no runtime behaviour change.\n\nReferences:\nRefs #1286\n\n---------\n\nCo-authored-by: PJ Fanning \u003cpjfanning@users.noreply.github.com\u003e"
    },
    {
      "commit": "2c5b6f7726d1a424ba7a4bf35368f8341bdda6cc",
      "tree": "6760317e742de5c302b78a15fc9ffef93f0e5fca",
      "parents": [
        "664928898317e4fa702236828eb7b8d5467dfe73"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 08 21:02:44 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 20:02:44 2026 +0100"
      },
      "message": "Update sbt-scalafix, scalafix-core to 0.14.8 (#1285)"
    },
    {
      "commit": "664928898317e4fa702236828eb7b8d5467dfe73",
      "tree": "c17b048a0d27a5179fb048d2255ab689df71ac73",
      "parents": [
        "bc5af65a3b5b30371be27c90e88271cc4f370c26"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 08 21:02:21 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 20:02:21 2026 +0100"
      },
      "message": "Update scala-xml to 2.5.0 (#1288)"
    },
    {
      "commit": "bc5af65a3b5b30371be27c90e88271cc4f370c26",
      "tree": "2d4137f6619c3e93f8dc3de0a7adbc51f7709155",
      "parents": [
        "c7655e129cb7630aaaa069fc9b7fb72b8950dfcf"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Tue Sep 08 21:01:54 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 08 20:01:54 2026 +0100"
      },
      "message": "Update classgraph to 4.8.195 (#1287)"
    },
    {
      "commit": "c7655e129cb7630aaaa069fc9b7fb72b8950dfcf",
      "tree": "3f48535f71a921a0387718365d310cb44168ddf6",
      "parents": [
        "def9e1b80c0e04fd3ce69aa95e3e987c627d022a"
      ],
      "author": {
        "name": "Samuele Resca",
        "email": "samuele.resca@gmail.com",
        "time": "Sun Sep 06 13:38:42 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 06 13:38:42 2026 +0100"
      },
      "message": "docs: modernize HTTP/2 ALPN and h2c guidance (#1253)\n\n* docs: modernize HTTP/2 ALPN and h2c guidance\n\nMotivation:\nThe HTTP/2 server-side doc still described ALPN as requiring separate\nnegotiation setup and documented a JDK \u003c\u003d 9 fallback where curl could not\ndetermine h2 support, plus it presented the HTTP/1.1 h2c Upgrade mechanism\nwithout noting it is now obsolete per RFC 9113. Apache Pekko HTTP\u0027s minimum\nsupported JDK has since moved to 17, where ALPN is natively part of JSSE\nand the RFC 7540 Upgrade path has been superseded.\n\nModification:\nReword the ALPN section to state it needs no extra provider or boot\nclasspath configuration on supported JDKs, and link to RFC 7301/RFC 9113\ninstead of RFC 7540. Add a warning that h2c Upgrade is obsolete in RFC 9113\nand should only be used for older-client compatibility, rename the \"h2c\nwith prior knowledge\" section to \"Cleartext HTTP/2 with prior knowledge\",\nand drop the outdated JDK \u003c\u003d 9 curl fallback example that no longer applies\non the project\u0027s supported JDKs.\n\nResult:\nThe doc reflects the project\u0027s current JDK baseline and RFC 9113 guidance\nwithout the stale JDK \u003c\u003d 9 ALPN caveat or unlabeled deprecated h2c Upgrade\nmechanism.\n\nTests:\nNot run - docs only\n\nReferences:\nNone - documentation accuracy update\n\n* Fixing wording from prior knowledge to Prior Knowledge"
    },
    {
      "commit": "def9e1b80c0e04fd3ce69aa95e3e987c627d022a",
      "tree": "5610d3a08d07ab716b9015bbe7ea8358c5d7510e",
      "parents": [
        "2dc91fef995e89564186fcdf927dccde2193f462"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Sep 06 12:42:20 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 06 12:42:20 2026 +0100"
      },
      "message": "fix: ignore the reserved bit of an incoming HTTP/2 stream identifier (#1261)\n\nMotivation:\n`Http2FrameParsing` read the stream identifier with a plain\n`readIntBE()`, keeping the reserved high bit, and carried a TODO saying\nthe bit should be checked. RFC 9113 5.1.1 says the bit is reserved and\nMUST be ignored when receiving. Keeping it means a peer that sets it\nproduces a negative stream id, which never matches the stream the frame\nis really for: `streamFor` treats it as closed and the connection is\nfailed with a GOAWAY instead of the frame being handled on its stream.\n\nModification:\nMask the reserved bit off (`\u0026 0x7FFFFFFF`) when reading the identifier\nand replace the TODO with the rule it was standing in for.\n\nResult:\nA frame whose stream identifier carries the reserved bit is handled on\nthe stream it names, as the spec requires, rather than failing the\nconnection.\n\nTests:\n- sbt \"http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.framing.Http2FramingSpec\" - pass (189 tests); a new test sends HEADERS for stream 1 with the reserved bit set and expects the request to be dispatched. Verified it fails with the fix stashed (the connection is failed instead).\n- sbt http-core/mimaReportBinaryIssues - pass (internal impl.engine.http2 change, no public API).\n\nReferences:\nNone - ignores the reserved stream identifier bit per RFC 9113 5.1.1\n\nCo-authored-by: Claude Opus 5 (1M context) \u003cnoreply@anthropic.com\u003e"
    },
    {
      "commit": "2dc91fef995e89564186fcdf927dccde2193f462",
      "tree": "fe9a33fb44a0af2bb04c535c5a34965bdd262b8f",
      "parents": [
        "28def88cf11835602c14b0fb62039455b2496915"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Sep 06 12:05:28 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 06 12:05:28 2026 +0100"
      },
      "message": "fix: release connection-level flow control accounting for discarded buffered data (#1259)\n\nMotivation:\n`totalBufferedData` feeds the connection-level flow controller, which\nemits a WINDOW_UPDATE only while `outstanding + buffered` stays below\nhalf of `incoming-connection-level-buffer-size`. It is incremented for\nevery DATA frame received but was not decremented on the three paths\nthat discard a non-empty buffer:\n\n- `IncomingStreamBuffer.onRstStreamFrame` cleared the buffer after the\n  peer reset the stream,\n- `IncomingStreamBuffer.onDownstreamFinish` cleared it when the\n  application cancelled the entity stream,\n- `CollectingIncomingData.onRstStreamFrame` was a no-op even though the\n  bytes collected so far had been counted.\n\nA peer that sends data the handler does not read and then resets the\nstream therefore ratchets `totalBufferedData` up permanently. Once the\nleaked total reaches half the configured buffer size the server stops\nreplenishing the connection window, it drains to zero and every stream\non that connection stalls.\n\nModification:\nAdd `IncomingStreamBuffer.discardBuffer()`, which subtracts what is\nstill buffered from `totalBufferedData` before clearing it, and use it\non both discard paths. Subtract the collected bytes in\n`CollectingIncomingData.onRstStreamFrame` for the same reason.\n\nResult:\nResetting or cancelling a stream releases the connection-level window\nits buffered data reserved, so the connection keeps being replenished\nand no longer stalls.\n\nTests:\n- sbt \"http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec org.apache.pekko.http.impl.engine.http2.Http2ClientSpec org.apache.pekko.http.impl.engine.http2.Http2ClientServerSpec\" - pass (174 tests); a new test buffers request data the handler never reads and resets the stream six times over, with the connection-level buffer size lowered to the initial window so the effect is reached quickly, then asserts the connection still accepts a request. Verified it fails with the fix stashed (the peer runs out of connection window).\n- sbt http-core/mimaReportBinaryIssues - pass (internal impl.engine.http2 change, no public API).\n\nReferences:\nNone - releases buffered-data accounting when a stream is reset or cancelled"
    },
    {
      "commit": "28def88cf11835602c14b0fb62039455b2496915",
      "tree": "b689fc34118e203e389e865d22ac949278dd30f6",
      "parents": [
        "44b01c355969a4e0a3083a1ca46522d234a92b74"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Sep 06 12:02:55 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 06 12:02:55 2026 +0100"
      },
      "message": "fix: require every Origin to be allowed on a CORS request (#1262)\n\nMotivation:\n`validateOrigins` accepted a request as soon as *one* of the origins in\nthe `Origin` header matched the allowed matcher, but the response\nechoes back *every* origin it was given\n(`CorsSettingsImpl.accessControlAllowOrigin` renders the whole list). A\nrequest carrying an allowed origin next to a disallowed one therefore\npassed validation and echoed the disallowed origin back in\n`Access-Control-Allow-Origin`. The pre-flight path is not affected\nbecause it already only accepts a single origin, so this was limited to\nsimple/actual requests.\n\nModification:\nRequire all origins to match rather than any, keeping the existing\n`allowed-origins \u003d *` short-circuit. The extra `nonEmpty` guard\npreserves today\u0027s handling of a `null` origin (an empty origin list),\nwhich must still be rejected when the allowed origins are restricted -\n`forall` alone would accept it.\n\nResult:\nAn actual request is accepted only when every origin it lists is\nallowed, so no origin that failed validation is echoed back. A request\nwhose origins are all disallowed is still rejected with\n`InvalidOrigin` listing them, as before.\n\nTests:\n- sbt http-cors/test - pass (47 tests); a new test asserts that an actual request listing a disallowed origin next to an allowed one is rejected with `InvalidOrigin`. Verified it fails with the fix stashed (the request is accepted and both origins are echoed). The existing \"there are two origins\" rejection test and the restricted-`null`-origin test both still pass.\n- sbt http-cors/mimaReportBinaryIssues - pass\n\nReferences:\nNone - requires all origins of a CORS request to be allowed"
    },
    {
      "commit": "44b01c355969a4e0a3083a1ca46522d234a92b74",
      "tree": "ffc072d45c0ae6b39d987ef716c7c1d74d0273d0",
      "parents": [
        "9876344e8a17fcb32d2e9e5e6e09d44d18eb5d11"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Sep 06 12:01:17 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 06 12:01:17 2026 +0100"
      },
      "message": "fix: reject a message whose Transfer-Encoding value cannot be parsed (#1267)\n\nMotivation:\nWhen a modelled header value fails to parse, `ModeledHeaderValueParser`\ncalls `onIllegalHeader` - which by default only logs - and degrades the\nheader to a `RawHeader`. For `Transfer-Encoding` that is unsafe: the\ndegraded header never reaches the `case h: Transfer-Encoding` arm in\n`parseHeaderLines`, so `isChunked` stays false and, with a\nContent-Length also present, the message is framed by Content-Length.\nAn upstream that does parse the value - stripping quotes, tolerating\ntrailing junk - frames the same message as chunked. The two disagree\nabout where the message ends, which is a request smuggling primitive.\n\nPekko HTTP already rejects the clear cases: `chunked` together with a\nContent-Length, an unsupported coding, and multiple entries. Only the\nunparseable value was silently tolerated.\n\nModification:\nFail the message when a `Transfer-Encoding` header arrives as a\n`RawHeader`. That can only happen when the modelled parse failed:\n`transfer-encoding` is in `alwaysParsedHeaders`, so it is modelled even\nwhen `modeled-header-parsing` is off, and a well-formed value always\nreaches the modelled arm.\n\nResult:\nA message whose Transfer-Encoding cannot be understood is rejected\nrather than framed by a different rule than the sender used.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.parsing.*\" - pass (246 tests); a new test sends `Transfer-Encoding: \"chunked\"` alongside a Content-Length and expects a 400. Verified it fails without the change, where the message is accepted and framed by Content-Length.\n- sbt http-core/mimaReportBinaryIssues - pass\n\nReferences:\nNone - rejects a message whose Transfer-Encoding value cannot be parsed"
    },
    {
      "commit": "9876344e8a17fcb32d2e9e5e6e09d44d18eb5d11",
      "tree": "78637fb5367b8294fa39e69bcedd2ee9b1114671",
      "parents": [
        "08b65ed9ed5db002f83afbcbf14f0be3c403b1b5"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Sep 06 12:00:19 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Sep 06 12:00:19 2026 +0100"
      },
      "message": "fix: honour the Materializer passed to Java DSL Route.flow (#1276)\n\nMotivation:\nRouteAdapter.flow(system, materializer) accepted a Materializer but only\nused the ActorSystem. The flow was built through the implicit\nRouteResult.routeToFlow conversion, and Route.toFunction hardcodes\nSystemMaterializer(system).materializer into the RequestContext, so the\nmaterializer supplied by Java users was silently discarded. Routes saw\nthe system materializer in extractMaterializer, entity unmarshalling and\nany stream run from a directive.\n\nModification:\nAdded a private[pekko] Route.toFunction(route, materializer) overload\nholding the existing body; the public toFunction(route) now delegates to\nit with SystemMaterializer(system).materializer, so its behaviour is\nunchanged. RouteAdapter.scalaFlow builds its handler with that overload\nand does the mapAsync(1) itself, so the given materializer reaches the\nRequestContext.\n\nResult:\nRoute.flow(system, materializer) runs the route with the materializer it\nwas given. flow(system) is unaffected: it already passed\nSystemMaterializer(system).materializer explicitly.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.javadsl.server.RouteFlowMaterializerTest\" - 2 passed; directional: with RouteAdapter reverted, flowUsesTheGivenMaterializer fails (expected: \u003c270352276\u003e but was: \u003c1155460560\u003e)\n- sbt \"http/scalafmt\" \"http-tests/Test/javafmt\" \"http/mimaReportBinaryIssues\" - clean\n- sbt headerCreateAll - header added to the new test file\n\nReferences:\nNone - reported by a maintainer against main"
    },
    {
      "commit": "08b65ed9ed5db002f83afbcbf14f0be3c403b1b5",
      "tree": "a3b669e5e5ccf9d3c7a9eb2a4a9878fbe7f68664",
      "parents": [
        "aa0225cbac48d47a1daadbdaafce15a90f561f73"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Fri Sep 04 19:25:24 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 04 18:25:24 2026 +0100"
      },
      "message": "Update scala-library to 3.9.0 (#1274)\n\n* Update scala-library to 3.9.0\n\n* Update Scala versions in Dependencies.scala\n\n---------\n\nCo-authored-by: PJ Fanning \u003cpjfanning@users.noreply.github.com\u003e"
    },
    {
      "commit": "aa0225cbac48d47a1daadbdaafce15a90f561f73",
      "tree": "6e48671f8c3e7585590b0569a4d504efe0d3b6b5",
      "parents": [
        "6cd9e7b80dd3c8eb23deeb49e36b2b9c0f82a912"
      ],
      "author": {
        "name": "Scala Steward",
        "email": "43047562+scala-steward@users.noreply.github.com",
        "time": "Fri Sep 04 19:25:03 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Sep 04 18:25:03 2026 +0100"
      },
      "message": "Update munit to 1.3.6 (#1275)"
    },
    {
      "commit": "6cd9e7b80dd3c8eb23deeb49e36b2b9c0f82a912",
      "tree": "ebb87a059178854cdaa9fda45530565cfd386fd1",
      "parents": [
        "b9a6a41ea13440910137ad3807a1c44f3f2bbc3a"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Wed Sep 02 09:22:53 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Sep 02 09:22:53 2026 +0100"
      },
      "message": "fix: drop rendered headers containing NUL as well as CR and LF (#1260)\n\nMotivation:\nThe guard in `Rendering.~~(HttpHeader)` renders a header and then scans\nthe rendered bytes, discarding the header if it finds one of the\ncharacters that must never reach the wire. It only looked for CR and\nLF, so a `RawHeader` value carrying a NUL was rendered as-is. NUL is\nnot a legal field-value character, and a downstream consumer that\ntreats the value as a C string truncates it there, so two parties can\ndisagree about where the value ends. The HTTP/2 renderer rejects CR, LF\nand NUL alike, so HTTP/1.1 was the weaker of the two.\n\nModification:\nAdd `Rendering.isIllegalHeaderChar`, which covers CR, LF and NUL, and\nuse it from all four `check` implementations (`StringRendering`,\n`ByteArrayRendering`, `ByteStringRendering` and\n`CustomCharsetByteStringRendering`) instead of repeating the character\ncomparison a fourth time. It takes an `Int` so the `Char` and `Byte`\nbased renderings can pass their element straight in.\n\nResult:\nA header whose name or value contains NUL is discarded like one\ncontaining CR or LF, on every rendering implementation, and the rule\nnow lives in one place.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.util.RenderingSpec org.apache.pekko.http.impl.engine.rendering.ResponseRendererSpec org.apache.pekko.http.impl.engine.rendering.RequestRendererSpec\" - pass (80 tests); a new case in the shared rendering table asserts a header with NUL in the value is discarded. Verified it fails with the fix stashed, once for each of the four renderings.\n- sbt http-core/mimaReportBinaryIssues - pass (internal impl.util change, no public API).\n\nReferences:\nNone - extends the outgoing header guard to NUL"
    },
    {
      "commit": "b9a6a41ea13440910137ad3807a1c44f3f2bbc3a",
      "tree": "7fa878c97cf92ec7e09409b7d1f7bad2f99ac461",
      "parents": [
        "46d2ffc4002cdb28f7ad16c4dadeeb417058c8a7"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 20:55:14 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 20:55:14 2026 +0100"
      },
      "message": "fix: prevent CRLF injection through chunk trailers and extensions (#1256)\n\nMotivation:\n`RenderSupport.renderChunk` emitted two attacker-influenced parts of a\nchunked response without the CR/LF guard that the main header block\nrelies on:\n\n- Trailer headers were rendered with `r ~~ trailer`, which resolves to\n  the generic sequence renderer and calls `header.render` directly,\n  bypassing the `~~(HttpHeader)` overload whose `check` scans the\n  rendered bytes for CR/LF and drops the header. `HttpEntity.LastChunk`\n  and `RawHeader` do no CR/LF validation, so a trailer built from user\n  data (e.g. `RawHeader(\"X-Trace\", \"ok\\r\\nSet-Cookie: ...\"))`) could\n  split the response. The identical header placed in the main header\n  block is caught; only the trailer path let it through.\n- The chunk extension was rendered raw into the chunk-size line\n  (`r ~~ \u0027;\u0027 ~~ extension`) with no CR/LF check, so a CR/LF in an\n  app-set extension corrupted the chunk framing.\n\nModification:\nRender each trailer header through the guarded `~~(HttpHeader)`\noverload (`trailer.foreach(r ~~ _)`), matching the main header block,\nand remove the now-unused `trailerRenderer` implicit so the unguarded\npath cannot be reintroduced by accident. Skip a chunk extension that\ncontains CR/LF; the extension is optional metadata, so omitting an\nillegal one is safe. Byte output is unchanged for valid trailers and\nextensions.\n\nResult:\nCR/LF in a chunk trailer header value or a chunk extension can no\nlonger reach the wire; the offending header/extension is dropped, as\nin the main header block, instead of splitting the response.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.rendering.ResponseRendererSpec org.apache.pekko.http.impl.engine.rendering.RequestRendererSpec\" - pass (62 tests); two new tests assert a CRLF-bearing trailer header and a CRLF-bearing chunk extension are dropped. Verified both fail with the fix stashed (the injected bytes reach the output).\n- sbt http-core/mimaReportBinaryIssues - pass (internal impl.engine.rendering change, no public API).\n\nReferences:\nNone - closes the CRLF-injection paths in chunked response rendering"
    },
    {
      "commit": "46d2ffc4002cdb28f7ad16c4dadeeb417058c8a7",
      "tree": "5fac60e480c5f2579757c32ad421409af966c9c6",
      "parents": [
        "2bb837f5543cc35ddbe61d51fdb64deade1398f9"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 20:38:28 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 20:38:28 2026 +0100"
      },
      "message": "fix: drop HTTP/2 header fields containing CR, LF or NUL (#1258)\n\nMotivation:\nOutgoing HTTP/2 header fields were HPACK-encoded without checking for\nCR, LF or NUL in the name or value. The HTTP/1.1 renderer scans each\nrendered header for CR/LF and drops it, but the HTTP/2 path had no\nequivalent, so an attacker-influenced value (a RawHeader, CustomHeader\nor a response trailer built from user data) was encoded verbatim. RFC\n9113 8.2.1 forbids these characters in a field name or value. On a\nnative HTTP/2 leg HPACK is length-prefixed so this is not direct frame\nsplitting, but it violates the spec and enables HTTP/2 -\u003e HTTP/1.1\ndowngrade smuggling when an intermediary re-serialises the message,\nand it means the mitigation an application relies on under HTTP/1.1\nsilently disappears under HTTP/2.\n\nModification:\nIn `HeaderCompression`, the single point every outgoing header field\n(regular headers, trailers and pseudo-headers all arrive here as\nkey/value pairs) passes through before HPACK encoding, drop any field\nwhose name or value contains CR, LF or NUL, logging at debug. Debug\nrather than warning because the value can be attacker-influenced, so a\nwarning would be a log-flooding vector, and the HTTP/1.1 renderer drops\nsilently too.\n\nResult:\nCR/LF/NUL in an HTTP/2 header name or value can no longer reach the\nwire; the offending field is dropped, matching the HTTP/1.1 renderer,\nwhether it rides in the header block or a response trailer.\n\nTests:\n- sbt \"http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec\" - pass (125 tests); two new tests assert a CRLF-bearing response header and a CRLF-bearing response trailer header are dropped while a valid sibling trailer survives. Verified both fail with the fix stashed (the injected set-cookie reaches the decoded headers).\n- sbt http-core/mimaReportBinaryIssues - pass (internal impl.engine.http2 change, no public API).\n\nReferences:\nNone - aligns HTTP/2 header rendering with the HTTP/1.1 CR/LF guard (RFC 9113 8.2.1)"
    },
    {
      "commit": "2bb837f5543cc35ddbe61d51fdb64deade1398f9",
      "tree": "3fc2e77837f474cbde3a57f46c468e98fdcde44c",
      "parents": [
        "4fab7d6f40894bae1576ed5421f4e6b47f656cd8"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 11:54:59 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 11:54:59 2026 +0100"
      },
      "message": "Compare path elements when checking that a served file is below the base directory (#1218)\n\n* compare path elements when checking that a file is below the served directory\n\nMotivation:\n`checkIsSafeDescendant` compared the canonical location of the requested\nfile with the canonical path of the served directory as a plain string\nprefix. A path such as `/var/www-private/secret` has `/var/www` as a string\nprefix without being contained in it, so a symbolic link inside the served\ndirectory that resolves to such a sibling directory passed the check and the\nfile was served. The segment filter in `safeJoinPaths` does not catch this,\nbecause no path segment is suspicious; only canonicalization moves the\nlocation out of the directory.\n\nModification:\nCompare the two canonical paths element by element via `java.nio.file.Path`\ninstead of as strings. That keeps the base directory itself accepted, which\nthe directory listing of `getFromBrowseableDirectory` relies on.\n\nResult:\nOnly files that are really below the served directory are served, and a\nsymbolic link to a sibling directory is rejected with the existing warning\nregardless of how the sibling is named.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.server.directives.FileAndResourceDirectivesSymlinkSpec\" - pass, 1 new test that serves a symlink to a sibling directory named after the served one; it fails without the change\n- sbt http-tests/test - pass\n- sbt http/mimaReportBinaryIssues - pass\n- sbt http/scalafmt http-tests/Test/scalafmt - clean\n\nReferences:\nNone - tightens the containment check for file and resource directives\n\n* reject paths that cannot exist instead of erroring, document the element-wise containment\n\nMotivation:\nReview of the containment change found that Paths.get can throw an\nunchecked InvalidPathException where the previous String.startsWith\nnever threw: on Windows a decoded segment such as `a\u003cb` passes\nsafeJoinPaths and File.getCanonicalPath but not the stricter\nWindowsPathParser, turning a previously clean rejection into a 500\nthat skips the traversal warning and breaks rejection-based route\nchaining. A NUL byte in a segment (`%00`) had the same effect on every\nplatform even before this PR, via the IOException that\nFile.getCanonicalPath throws for it. The scaladoc above\nsafeDirectoryChildPath also still described the string-prefix\ncontainment semantics that the previous commit removed as insecure,\nand the symlink spec wrote file content that no assertion reads.\n\nModification:\nCatch InvalidPathException and IOException in checkIsSafeDescendant\nand treat such paths as not contained, producing the existing warning\nand rejection; no file with such a name can exist, so nothing\nservable is lost. Restate the scaladoc containment contract as\nelement-wise comparison and note that Windows canonicalization does\nnot resolve NTFS symbolic links or junctions. Create the sibling\nfixture file empty in the symlink spec. Rebased onto main.\n\nResult:\nA request segment that no file-system path may contain is rejected\nwith the traversal warning instead of escaping to the exception\nhandler as a 500, on all platforms; the documented contract matches\nthe implementation.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.server.directives.FileAndResourceDirectivesSpec org.apache.pekko.http.scaladsl.server.directives.FileAndResourceDirectivesSymlinkSpec\" - pass (52 tests); new test rejects a %00 segment and asserts the traversal warning; verified it fails with the fix stashed (unhandled IOException)\n- sbt http/mimaReportBinaryIssues - pass\n- native scalafmt run on the three changed files - clean\n\nReferences:\nNone - hardens the containment check follow-up from review"
    },
    {
      "commit": "4fab7d6f40894bae1576ed5421f4e6b47f656cd8",
      "tree": "7ac84ccf70de9449f14484a3f3bcdb8ffc441962",
      "parents": [
        "2f88aaedecd70570a79fabcb6a5092ccc3f83df0"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 11:50:37 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 11:50:37 2026 +0100"
      },
      "message": "fix: count repeated Connection headers towards max-header-count (#1255)\n\nMotivation:\n`parseHeaderLines` merges repeated `Connection` headers into a single\naccumulated header via `x.append(h.tokens)`, but the merge branch\nre-entered the loop with `headerCount` unchanged. The\n`headerCount \u003c settings.maxHeaderCount` guard therefore never tripped\non `Connection` headers, so a single message could carry an unbounded\nnumber of them. Because `Connection.append` is `Connection(this.tokens\n++ tokens)` — an O(n) copy of the accumulated token list on each\nheader — and there is no total-header-block byte limit, N repeated\n`Connection` headers drive ~N²/2 token copies and an N-element list\nfrom one request. Every other non-deduplicated header increments the\ncount; only this accumulating path did not.\n\nModification:\nIncrement `headerCount` in the `Connection` merge branch so each\nrepeated `Connection` header counts towards `max-header-count`, exactly\nas the generic header path does. Once the limit is reached the existing\nguard rejects the message with the standard \"more than the configured\nlimit\" error, bounding the number of merges to `max-header-count`.\n\nResult:\nA flood of `Connection` headers is rejected at `max-header-count`\n(default 64) instead of accumulating unboundedly, so the parser\u0027s\nwork is bounded by the configured limit like every other header.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.parsing.RequestParserCRLFSpec org.apache.pekko.http.impl.engine.parsing.RequestParserLFSpec\" - pass (116 tests); two new tests assert that exceeding max-header-count is rejected both for distinct headers and for repeated Connection headers. Verified the Connection test fails with the fix stashed (the flood is never caught).\n- sbt http-core/mimaReportBinaryIssues - pass\n\nReferences:\nNone - bounds repeated Connection header accumulation to max-header-count"
    },
    {
      "commit": "2f88aaedecd70570a79fabcb6a5092ccc3f83df0",
      "tree": "bb6e80bc789b7b6cc824188c206ab77ed1690080",
      "parents": [
        "f9859975230740340d3f23f4a7e7b0b6f0209375"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 11:35:47 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 11:35:47 2026 +0100"
      },
      "message": "fix: do not send a response body to HTTP/2 HEAD requests (#1239)\n\n* fix: do not send a response body to HTTP/2 HEAD requests\n\nMotivation:\nThe HTTP/2 engine had no notion of the HEAD method, which #1238 pinned as three\nfailing expectations:\n\n- the response entity was emitted as DATA frames, although RFC 9110 section\n  9.3.2 says a server MUST NOT send content in a response to a HEAD request\n- `transparent-head-requests` was applied only by HttpServerBluePrint, so it had\n  no effect over HTTP/2\n- a 304 (and a 204) got `content-length: 0`, where HTTP/1.1 omits the header;\n  RFC 9110 section 15.4.5 expects a 304 to carry the content-length a 200 would\n  have had, so a zero is actively misleading\n\nThe response path only ever sees an HttpResponse plus its stream id, so\nResponseRendering cannot know the request method. Carrying it on an attribute\nwould only work for the `bind` API, since `bindFlow` users copy the stream id\nattribute by hand.\n\nModification:\nTrack the method where it is already known per connection. Http2StreamHandling\nrecords the stream ids of incoming HEAD requests when the request HEADERS frame\nopens the stream, and handleOutgoingCreated cancels the response data and sends\nthe initial headers with endStream set for those streams. The header pairs are\nleft untouched, so the peer still learns the content-length it would have got\nfor a GET. Entries are removed when the response is created and when the stream\ncloses.\n\nBecause that tracking reads the method off the wire below the HTTP layer, it is\nunaffected by RequestParsing rewriting HEAD to GET, so transparent-head-requests\ncan now be honoured for HTTP/2 the same way HttpServerBluePrint honours it for\nHTTP/1.1.\n\nResponseRendering gains the status based part of the rules HTTP/1.1 applies via\nHttpMethod.contentLengthAllowed, so 1xx, 204 and 304 no longer render a\ncontent-length.\n\nResult:\nA HEAD request over HTTP/2 gets headers only, with the content-length the\nresource would have had, and transparent-head-requests behaves as it does for\nHTTP/1.1.\n\nTests:\n- sbt \"http2-tests / test\" - 349 passed, 25 ignored, 26 pending\n- sbt +mimaReportBinaryIssues - success\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n- git diff --check - clean\n\nReferences:\nRefs #1236, Refs #1238\n\n* test: cover the status based content-length rules and CONNECT over HTTP/2\n\nMotivation:\nPR #962 introduced two sets of rules in HttpMethod.contentLengthAllowed: a\nmethod specific one for HEAD and CONNECT, and a status based one that applies to\nevery method. HTTP/2 had no coverage for either. The 204 and 304 cases added\nalongside the HEAD fixes were exercised with a HEAD request only, even though\nthe status based rules do not depend on the method, so a regression on GET would\nhave gone unnoticed.\n\nModification:\nMove the 204 and 304 cases out of the HEAD section into a new section that\ndrives them with a plain GET, and add the 200 and 205 cases. 205 is the status\nPR #962 was really aimed at: RFC 9112 section 6.3 exempts only 1xx, 204 and 304\nfrom framing, so a 205 must be framed, and it is what http4s/http4s#7919\nreports against another server.\n\nAdd a RequestParsingSpec case pinning that a CONNECT request is rejected,\nbecause neither RFC 9113 section 8.5 CONNECT nor RFC 8441 extended CONNECT is\nsupported: the request omits \":scheme\" and \":path\", which the parser treats as\nmandatory. That makes it explicit that the CONNECT specific rule in\nHttpMethods.contentLengthAllowed is unreachable over HTTP/2.\n\nResult:\nThe status based rules are covered independently of the request method, and the\nabsence of CONNECT support is recorded rather than assumed.\n\nTests:\n- sbt \"http2-tests / test\" - 352 passed, 25 ignored, 26 pending\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n- git diff --check - clean\n\nReferences:\nRefs #1236, Refs #962"
    },
    {
      "commit": "f9859975230740340d3f23f4a7e7b0b6f0209375",
      "tree": "8e54f779afea6c21692c19005654794f88d1413e",
      "parents": [
        "c7f3b45818119c176f72a31fa6b5166d9e56d4b2"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 11:21:56 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 11:21:56 2026 +0100"
      },
      "message": "perf: find SSE line terminators with ByteString.indexOf (#1226)\n\nMotivation:\nLineParser scanned its buffer one byte at a time with `bs(at)`. The buffer is\nbuilt as `buffer ++ grab(in)`, so it is a multi-fragment ByteString whenever a\nline spans several chunks, and `ByteStrings.apply` walks the fragment list from\nthe first fragment on every access. Parsing an SSE line that arrives in many\nchunks therefore costs O(bytes * fragments).\n\nModification:\nLocate the next CR or LF with ByteString.indexOf, which is fragment aware and\nscans several bytes at a time, instead of testing every byte. The three\nidentical line-emitting blocks are factored into a local helper. Line\ntermination semantics (CR, LF, CRLF, and a CRLF split across chunks) are\nunchanged.\n\nResult:\nLarge SSE lines delivered in small chunks parse dramatically faster; small\nlines are unaffected.\n\n  lineSize  chunkSize      before        after\n      1 KB        512    0.265 ms     0.189 ms\n      1 KB       8192    0.295 ms     0.183 ms\n    128 KB        512   71.210 ms     1.013 ms\n    128 KB       8192    3.269 ms     0.342 ms\n      1 MB        512  9821.281 ms   54.361 ms\n\nTests:\n- sbt \"http-tests / Test / testOnly org.apache.pekko.http.scaladsl.unmarshalling.sse.*\" - 48 passed (46 existing plus 2 new)\n- sbt \"http-bench-jmh/Jmh/run -f 1 -wi 3 -i 3 -p lineSize\u003d... -p chunkSize\u003d... .*LineParserBenchmark.*\" - numbers above, JDK 21\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n\nReferences:\nNone - found while auditing ByteString usage across the code base"
    },
    {
      "commit": "c7f3b45818119c176f72a31fa6b5166d9e56d4b2",
      "tree": "7ec449b3a0b025d7c60c20a4128190a8d7fecaaf",
      "parents": [
        "98fd100ea75fe82be62a0e1c372880415aa73eb0"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 08:55:28 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 08:55:28 2026 +0100"
      },
      "message": "Avoid reopening jars per resource request and deleteOnExit per upload (#1217)\n\n* serve jar resources without reopening the jar per request\n\nMotivation:\n`ResourceFile` opened a `java.util.zip.ZipFile` for every request to a\nresource that lives in a jar, only to read the entry\u0027s size and time. That\nparses the whole central directory of the jar again per request, and\n`getFromResource`/`getFromResourceDirectory` served from a jar is the usual\nproduction layout for static resources. The result of `getEntry` was also\ndereferenced without a null check.\n\nModification:\nRead the metadata from the `JarURLConnection` instead and leave its cache\nenabled, so the JDK reuses the same open jar file that the class loader\nalready holds. Guard against a null entry, and share the plain\n`URLConnection` handling with the fallback branch.\n\nResult:\nNo jar is opened or parsed per request for resources served from a jar, and\na missing entry rejects the request instead of throwing.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.server.directives.FileAndResourceDirectivesSpec\" - pass, 1 new test asserting the entry metadata matches the bytes served\n- sbt http-tests/test - pass\n- sbt +http/compile - pass\n- sbt http/mimaReportBinaryIssues - pass\n- sbt http/scalafmt http-tests/Test/scalafmt - clean\n\nReferences:\nNone - avoids reopening jars for every resource request\n\n* don\u0027t register every uploaded temp file with deleteOnExit\n\nMotivation:\n`fileUploadAll` called `File.deleteOnExit()` for each temporary upload file.\nThe JVM keeps every path passed to `deleteOnExit` in a global set for the\nlifetime of the process, and the entry is not removed when the file itself\nis deleted after the stream is consumed. A long-running server accepting\nuploads therefore grows its heap by one entry per upload, forever.\n\nModification:\nPut the temporary upload files in a directory of their own and register a\nsingle shutdown hook that removes that directory recursively on exit.\n\nResult:\nThe on-exit cleanup that the directive documents is unchanged, but it now\ncosts one shutdown hook per JVM instead of one permanent global entry per\nuploaded file. The dedicated directory is created with the owner-only\npermissions that `Files.createTempDirectory` applies.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.server.directives.FileUploadDirectivesSpec\" - pass, 1 new test asserting the temp files share one directory\n- sbt http-tests/test - pass\n- sbt +http/compile - pass\n- sbt http/mimaReportBinaryIssues - pass\n- sbt http/scalafmt http-tests/Test/scalafmt - clean\n\nReferences:\nNone - removes an unbounded deleteOnExit registration per upload\n\n* make the jar file cache for resource serving configurable\n\nMotivation:\nReading jar resource metadata through the JDK\u0027s jar file cache means the\njar file stays open for the lifetime of the process, which prevents the jar\nfrom being replaced while the server runs (on Windows an open file cannot\nbe replaced). That should be a choice rather than something the directives\ndecide.\n\nModification:\nAdd a `pekko.http.routing.use-jar-file-cache` setting, on by default, and\npass it from `getFromResource` into `ResourceFile`. With the setting off,\nthe connection that reads the entry metadata owns its jar file and closes\nit again, and the entity stream is opened through a connection with caches\ndisabled as well, so that nothing keeps the jar open between requests.\n`ResourceFile.apply(url)` keeps its previous meaning and uses the cache.\n\nResult:\nThe default is the cached behaviour, and deployments that need to replace\njar files at runtime can turn the cache off. Note that the previous\nimplementation could not offer that at all: it opened its own `ZipFile` for\nthe metadata but still streamed the content through `URL.openStream`, which\nuses the JDK caches.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.server.directives.FileAndResourceDirectivesSpec\" - pass, 1 new test serving a jar resource with the cache disabled\n- sbt http-tests/test - pass (TimeoutDirectivesSpec flaked in the full run, passes on its own)\n- sbt +http/mimaReportBinaryIssues - pass\n- sbt http/scalafmt http-tests/Test/scalafmt - clean\n\nReferences:\nNone - follow-up to the jar resource change on this branch\n\n* address review: shutdown ordering, jar connection ownership, config claims\n\nMotivation:\nReview of the branch found problems in both halves. The raw\nRuntime.addShutdownHook ran concurrently with CoordinatedShutdown\u0027s\nhook, so during a graceful drain it could delete temp files that\nin-flight uploads still use - deleteOnExit provably deleted only after\napplication hooks finished. The memoized upload directory was never\nrecreated if a temp-file reaper removed it while empty, failing all\nlater uploads until restart. In the jar path, the cache-off close was\nnot in a finally (leaking a JarFile per failing request), an explicit\nsetUseCaches(true) silently defeated an application-wide\nURLConnection.setDefaultUseCaches(false), the public one-arg\nResourceFile.apply changed semantics by pinning jars in the JDK cache,\ngetContentLength truncated and turned unknown lengths into silent\nempty 200s, an exception from the close in fromUrlConnection\u0027s finally\nbecame a 500 where callers expect a rejection, and the cache decision\nwas spread over three hand-synchronized places. The use-jar-file-cache\ndocumentation also wrongly claimed the JDK\u0027s jar cache is the class\nloader\u0027s cache and implied disabling it makes classpath jars\nreplaceable.\n\nModification:\nMake UploadTempFiles a per-actor-system extension whose directory is\nremoved by a CoordinatedShutdown task in the actor-system-terminate\nphase, after the drain; recreate the directory in create() if it is\ngone. In ResourceFile, restore the one-arg apply to its historical\nno-handle-kept semantics (useJarFileCache \u003d false), decide jar\nownership from the connection\u0027s effective getUseCaches, close the\nowned jar in a guarded finally, only call setUseCaches when disabling,\ncentralize that rule in one openConnection helper shared with\nopenStream, dispatch on the connection type instead of the protocol\nstring, use getContentLengthLong and reject unknown lengths, and map\nFileNotFoundException to None with a guarded stream close. Reword the\nreference.conf entry to scope the replaceability promise to jars no\nclass loader holds open and document the two-parse cost of cache-off\nmode. Rebased onto main.\n\nResult:\nUpload temp files are deleted only after the system has drained and\nuploads keep working if the temp directory disappears; the jar path\nneither leaks handles nor overrides application-wide cache opt-outs;\nexternal ResourceFile(url) callers keep the pre-existing behavior; and\nthe configuration text makes no false claims about the JDK.\n\nTests:\n- sbt \"http-tests/testOnly org.apache.pekko.http.scaladsl.server.directives.FileUploadDirectivesSpec org.apache.pekko.http.scaladsl.server.directives.FileAndResourceDirectivesSpec\" - pass (73 tests); new tests cover directory recreation after removal and directory deletion when a separate actor system terminates\n- sbt http/mimaReportBinaryIssues - pass\n- sbt \"+http/compile\" - pass on 2.13.18 and 3.3.8\n- native scalafmt run on the changed Scala files - clean\n\nReferences:\nRefs #1242 - keeps the threat model\u0027s \"registers no JVM shutdown hook\" claim true"
    },
    {
      "commit": "98fd100ea75fe82be62a0e1c372880415aa73eb0",
      "tree": "948dd4eaa3bc5c9098352bedd1911589903889a2",
      "parents": [
        "b67759ca1229a1d7e848345f6f8aca4b5f0bd4fc"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Tue Sep 01 08:54:49 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Sep 01 08:54:49 2026 +0100"
      },
      "message": "test: cover Locale.ROOT lowercasing paths from #1086 (#1225)\n\nMotivation:\n#1086 switched a dozen call sites to Locale.ROOT but landed without tests, so\nTurkishISpec still only covers HttpCharsets and nothing exercises the directives.\nA regression in any of these paths would only show up for users running with a\nturkish default locale.\n\nModification:\nExtend TurkishISpec with cases for `HttpHeader.parse`, `Uri` scheme\nnormalization, `MediaTypes.forExtension` and `ErrorInfo.withErrorHeaderName`.\nAdd TurkishLocaleDirectivesSpec covering `headerValueByName` and\n`overrideMethodWithParameter`, which live in the http module.\n\nResult:\nThe header-name, scheme, file-extension, error-header-name, header-directive and\nmethod-override paths are pinned against locale sensitive case conversion.\n\nTests:\n- sbt \"http-core / Test / testOnly org.apache.pekko.http.scaladsl.model.TurkishISpec\" - 5 passed\n- sbt \"http-tests / Test / testOnly org.apache.pekko.http.scaladsl.server.directives.TurkishLocaleDirectivesSpec\" - 2 passed; both fail when the two directives are reverted to default-locale case conversion\n- sbt headerCreateAll - added the Apache header to the new file\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n\nReferences:\nRefs #1086"
    },
    {
      "commit": "b67759ca1229a1d7e848345f6f8aca4b5f0bd4fc",
      "tree": "4a0ea5cda4c2e4572bb922c71274baedef115af3",
      "parents": [
        "ef8a03f0d3e27667c7d3006bac24dadd13c7cddd"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Mon Aug 31 22:26:35 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Aug 31 22:26:35 2026 +0100"
      },
      "message": "stop parsing chunks once the chunk count limit is reached (#1220)\n\nMotivation:\nThe `max-chunk-count` check in `HttpMessageParser.parseChunkBody` has no\n`else` branch, so the `StateResult` of `failEntityStream` is discarded and\nparsing continues after the error has been emitted: the current chunk is\nemitted, the parser trampolines into the next one, and every following\nchunk in the buffer emits another error. The parser therefore keeps\nproducing output after it has set itself to terminated. A consumer that\nstops at the first error does not notice, which is why the behaviour has\ngone unseen; the parser still does the work.\n\nModification:\nPut the rest of `parseChunkBody` into the `else` branch of the check, the\nway the other limits in this parser are written.\n\nResult:\nNothing is emitted after the chunk count error, and the parser stops\nparsing the rest of the body.\n\nTests:\n- sbt \"http-core/testOnly org.apache.pekko.http.impl.engine.parsing.RequestParserCRLFSpec org.apache.pekko.http.impl.engine.parsing.RequestParserLFSpec\" - pass, 1 new test that collects the raw parser output; without the change it sees the chunks \"a\", \"b\", \"c\", \"\" instead of \"a\", \"b\"\n- sbt http-core/test - pass\n- sbt http-core/mimaReportBinaryIssues - pass\n- sbt http-core/scalafmt http-core/Test/scalafmt - clean\n\nReferences:\nNone - makes the max-chunk-count limit stop parsing"
    },
    {
      "commit": "ef8a03f0d3e27667c7d3006bac24dadd13c7cddd",
      "tree": "bf5010875c83ba6fe0c801f56a843d42094bfcdf",
      "parents": [
        "4638cdd5279795669aa45c260e53d29a2fb57070"
      ],
      "author": {
        "name": "dependabot[bot]",
        "email": "49699333+dependabot[bot]@users.noreply.github.com",
        "time": "Mon Aug 31 16:26:01 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Aug 31 16:26:01 2026 +0100"
      },
      "message": "build(deps): Bump actions/setup-java from 5.7.0 to 6.0.0 (#1254)\n\nBumps [actions/setup-java](https://github.com/actions/setup-java) from 5.7.0 to 6.0.0.\n- [Release notes](https://github.com/actions/setup-java/releases)\n- [Commits](https://github.com/actions/setup-java/compare/b6effb05e454b25005698d916606bdc6ffcbf961...dd06d9cba3e5552c54d9f8ea23572deb30010f7c)\n\n---\nupdated-dependencies:\n- dependency-name: actions/setup-java\n  dependency-version: 6.0.0\n  dependency-type: direct:production\n  update-type: version-update:semver-major\n...\n\nSigned-off-by: dependabot[bot] \u003csupport@github.com\u003e\nCo-authored-by: dependabot[bot] \u003c49699333+dependabot[bot]@users.noreply.github.com\u003e"
    },
    {
      "commit": "4638cdd5279795669aa45c260e53d29a2fb57070",
      "tree": "9097902e72fc626d42ca427e3ba06546e99f093d",
      "parents": [
        "85d7243e745d68c987b79c5c4d56958f582a5c34"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Mon Aug 31 09:53:50 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Aug 31 09:53:50 2026 +0100"
      },
      "message": "chore: fold Http2JDKAlpnSupport into Http2AlpnSupport (#1232)\n\nMotivation:\nHttp2AlpnSupport was a thin facade in front of Http2JDKAlpnSupport, split in two\npurely so that the ALPN classes would load lazily. Its scaladoc still says\n\"Will add support to an engine either using jetty alpn or using netty APIs\n(later)\" and \"We rely on lazy class loading to not fail with class loading\nerrors when ALPN support is missing\", and Http2.scala had a matching \"e.g. when\nALPN jar is missing\" comment. All of that dates from Java 8, where ALPN needed\nan external jar. This branch requires JDK 17, where ALPN is always available.\n\nWhile reading it I also found that clientSetApplicationProtocols ignored its\n`protocols` parameter and hardcoded Array(\"h2\"). That is invisible today because\nthe only caller passes exactly Array(\"h2\"), but it is a trap.\n\nModification:\nMerge the two objects into Http2AlpnSupport, make chooseProtocol private to it,\nand correct the scaladoc and the Http2.scala comment. Pass the `protocols`\nargument through to setApplicationProtocols instead of ignoring it. Drop\napplySessionParameters, a one-line delegate to TlsUtils that had no callers.\n\nBoth objects are @InternalApi, and removing Http2JDKAlpnSupport is a\nMissingClassProblem, so a mima-filters exclude file is added in the same way as\nthe existing remove-bytestringinputstream and remove-previewserversettings\nentries do for other removed impl classes.\n\nResult:\nOne object instead of two, no misleading documentation, and no silently ignored\nparameter. No behaviour change.\n\nTests:\n- sbt \"http2-tests / Test / testOnly ...Http2ClientServerSpec ...ProtocolSwitchSpec\" - 10 passed, 1 ignored. These negotiate h2 over TLS end to end, exercising both enableForServer and clientSetApplicationProtocols\n- sbt http-core/mimaReportBinaryIssues - clean with the new filter, and reports the two expected MissingClassProblems without it\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n\nReferences:\nNone - found while reviewing the code base against the JDK 17 baseline"
    },
    {
      "commit": "85d7243e745d68c987b79c5c4d56958f582a5c34",
      "tree": "e59169a722413286cb0a669878ea7d48398f3d85",
      "parents": [
        "09213dd239a8894359a59e04479b7d3fa39afc2a"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Aug 30 11:59:51 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Aug 30 11:59:51 2026 +0100"
      },
      "message": "Document Opentelemetry dependencies (#1240)\n\n* Document Opentelemetry dependency in startPoolInterface\n\nAdded note about Opentelemetry Java Instrumentation dependency in startPoolInterface method.\n\n* more doc"
    },
    {
      "commit": "09213dd239a8894359a59e04479b7d3fa39afc2a",
      "tree": "3ca188e5e54bc3dc39b0fc8e5686871e7206ebcc",
      "parents": [
        "0d8bed1ad2f6a58da342ada14ce85171ba160883"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Aug 30 11:57:21 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Aug 30 11:57:21 2026 +0100"
      },
      "message": "test: add HTTP/2 coverage for the HEAD method (#1238)\n\nMotivation:\nThe HTTP/2 engine had no test coverage for HEAD requests at all, and no HEAD\nhandling either: `grep HEAD` over the http2 engine and its tests returns only\nHEADERS frame matches. That left three divergences from HTTP/1.1 invisible.\n\nModification:\nAdd a \"support HEAD requests\" section to Http2ServerSpec covering request\ndelivery, content-length rendering for strict and known-length entities, the\nresponse body, `transparent-head-requests`, and a 304 response. Three of the\ntests pin behaviour that is currently wrong and carry a FIXME naming the cause\nand the relevant RFC, so that a fix has to flip the assertion deliberately\nrather than silently:\n\n- the response entity is emitted as DATA frames, although RFC 9110 section\n  9.3.2 says a server MUST NOT send content in a response to HEAD.\n  ResponseRendering only ever sees the HttpResponse plus its stream id, so the\n  engine cannot know the request was a HEAD.\n- `transparent-head-requests` is applied only by HttpServerBluePrint, so it has\n  no effect over HTTP/2 and the handler always sees a HEAD request.\n- HttpMessageRendering.addContentHeaders renders content-length straight from\n  the entity and never consults HttpMethod.contentLengthAllowed, so a 304 gets\n  `content-length: 0` where HTTP/1.1 omits the header.\n\nThe content-length tests confirm HTTP/2 already does the right thing for the\nobject-store use case from #1236.\n\nResult:\nHEAD over HTTP/2 is covered, and the three gaps are recorded as assertions\ninstead of being absent.\n\nTests:\n- sbt \"http2-tests / Test / testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec\" - 118 passed, 16 pending\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n- git diff --check - clean\n\nReferences:\nRefs #1236, Refs #1237"
    },
    {
      "commit": "0d8bed1ad2f6a58da342ada14ce85171ba160883",
      "tree": "ef24ad7453ab9b32beffb56b4fbd491f32f1b316",
      "parents": [
        "9eb283ed45d274cd948a9dae315be42ef98a50e1"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Aug 30 11:25:21 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Aug 30 11:25:21 2026 +0100"
      },
      "message": "chore: replace deprecated String constructor in StringTools (#1230)\n\nMotivation:\nStringTools.asciiStringFromBytes used the deprecated `new String(byte[], int)`\nbehind a @nowarn, justified by a comment saying it was \"the fastest way to\nconvert a ASCII encoded byte array into a String without extra copying\". That\nwas true on Java 8, where the alternative expanded the bytes into a char array.\nSince JDK 9 and compact strings it is not, and this branch requires JDK 17.\n\nThe method is on the HTTP/2 HPACK decode path, so it is worth being sure the\nreplacement is neither a behaviour nor a performance change.\n\nModification:\nDecode with ISO-8859-1, which maps every byte to the character of the same\nvalue, exactly as the deprecated constructor did with hibyte 0. Not US-ASCII,\nwhich would turn bytes above 0x7F into replacement characters; HPACK string\nliterals are opaque octets. Drops the @nowarn and the scala.annotation.nowarn\nimport, and reuses the ISO88591 constant already in this package.\n\nResult:\nNo deprecated JDK API and no misleading comment, with identical behaviour.\n\nTests:\n- New StringToolsSpec pins the byte to character mapping across the whole\n  0x00 to 0xFF range, which is what would break if the charset were changed to\n  US-ASCII later. It is a regression guard, not a failing-before test: this\n  change is deliberately behaviour preserving\n- Verified in jshell that the two forms produce equal Strings for all 256 byte\n  values\n- JMH, average time over sizes 12, 64 and 4096 bytes: 25.3 / 30.2 / 1260.2 ns\n  for the deprecated form against 28.2 / 43.1 / 952.7 ns for ISO-8859-1, error\n  bars overlapping at every size, so no measurable difference either way\n- sbt \"http-core / Test / testOnly org.apache.pekko.http.impl.util.StringToolsSpec\" - 4 passed\n- sbt http-core/mimaReportBinaryIssues - clean\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n\nReferences:\nNone - found while reviewing the code base against the JDK 17 baseline"
    },
    {
      "commit": "9eb283ed45d274cd948a9dae315be42ef98a50e1",
      "tree": "88567fa7c3d36d10884db4d65baa8134bc6d1d31",
      "parents": [
        "dfd5b18c85668a1fad0181dced693c0e8090340c"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Sun Aug 30 11:20:33 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Aug 30 11:20:33 2026 +0100"
      },
      "message": "fix: read HPACK string literals with readNBytes (#1231)\n\nMotivation:\nDecoder.readStringLiteral asked for a whole string literal with a single\n`in.read(buf)` and treated any shorter result as a decompression failure.\nInputStream.read(byte[]) is explicitly allowed to return fewer bytes than\nrequested even when more are available, so this relies on a contract the API\ndoes not offer.\n\nIt is not an observable bug today: the only caller wraps a compacted ByteString\n(HeaderDecompression compacts because the decoder needs mark/reset and\nmeaningful available()), and the decoder waits for `in.available() \u003e\u003d length`\nbefore reading, so the single read always fills the buffer. It is a trap for\nany future change to how that stream is produced.\n\nModification:\nUse InputStream.readNBytes, which loops until the requested number of bytes has\nbeen read or the stream ends, and compare the returned length. Available since\nJDK 11 and this branch requires JDK 17.\n\nResult:\nThe decoder no longer depends on a single read filling the buffer. Behaviour is\nunchanged for a stream that does fill it, and truncated input is still reported\nas a decompression failure.\n\nTests:\n- New HpackDecoderSpec round-trips header blocks through the shaded Encoder and\n  Decoder over a stream that has all its data available but returns 1, then 7,\n  bytes per read. Both cases fail before this change with a decompression\n  failure and pass after it\n- sbt \"http-core / Test / testOnly org.apache.pekko.http.impl.engine.http2.hpack.HpackDecoderSpec\" - 3 passed\n- sbt \"http2-tests / Test / testOnly org.apache.pekko.http.impl.engine.http2.RequestParsingSpec\" - 25 passed, 3 pending\n- sbt http-core/javafmtCheck and scalafmt - clean\n\nReferences:\nNone - found while reviewing the code base against the JDK 17 baseline"
    },
    {
      "commit": "dfd5b18c85668a1fad0181dced693c0e8090340c",
      "tree": "b0f318ac5d829bacfacb754278965d81449c182f",
      "parents": [
        "dd0a4a334bdb6a1908146a53eaf9064a1d181a22"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Fri Aug 28 21:06:24 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Aug 28 21:06:24 2026 +0100"
      },
      "message": "http/2: bound incoming header blocks with max-header-list-size (#1216)\n\nMotivation:\nThe HTTP/2 header decompression stage had no upper bound on the incoming\nside. The HPACK decoder was constructed with\nHttp2Protocol.InitialMaxHeaderListSize (Int.MaxValue) and the header block\nfragments of a HEADERS frame and its CONTINUATION frames were accumulated\nuntil END_HEADERS was seen, so the memory used for a single header block\nwas limited only by what the peer chose to send. Neither endpoint\nadvertised SETTINGS_MAX_HEADER_LIST_SIZE, so a peer had no way of knowing\nwhat it may send either.\n\nModification:\nAdd a `max-header-list-size` setting (64 KiB by default) to\n`pekko.http.server.http2` and `pekko.http.client.http2` and pass it to\n`HeaderDecompression`, which now\n\n* constructs the HPACK decoder with that limit and checks the truncation\n  result of `Decoder.endHeaderBlock()`, which was previously ignored,\n* applies the same limit to the accumulated header block fragments,\n  accounting each fragment with its frame header size so that the number\n  of empty CONTINUATION frames per header block is bounded as well,\n* fails the connection with GOAWAY(ENHANCE_YOUR_CALM) when the limit is\n  exceeded.\n\nThe configured value is advertised to the peer in the initial SETTINGS\nframe.\n\nResult:\nThe memory used for a single incoming header block is bounded by the\nconfigured limit on both the server and the client side, and peers are\ntold about the limit up front.\n\nTests:\n- sbt \"http2-tests/testOnly org.apache.pekko.http.impl.engine.http2.Http2ServerSpec\" - pass, 5 new tests\n- sbt http2-tests/test - pass\n- sbt http-core/test - pass (HostConnectionPoolSpec flaked in the full run, passes on its own)\n- sbt +http-core/mimaReportBinaryIssues - pass\n- sbt http-core/scalafmt http2-tests/Test/scalafmt - clean\n- sbt http-core/headerCreateAll - no changes\n\nReferences:\nNone - bounds the memory used for incoming HTTP/2 header blocks"
    },
    {
      "commit": "dd0a4a334bdb6a1908146a53eaf9064a1d181a22",
      "tree": "2b406d7ba5e3f8085a7153961689d1c51ed135cb",
      "parents": [
        "6d8605a71fe11c803ade411ad71b0b7a4c5ac882"
      ],
      "author": {
        "name": "PJ Fanning",
        "email": "pjfanning@users.noreply.github.com",
        "time": "Thu Aug 27 12:43:41 2026 +0100"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Aug 27 12:43:41 2026 +0100"
      },
      "message": "fix: render Content-Length for HEAD responses with a declared length (#1237)\n\nMotivation:\nPR #962 corrected Content-Length rendering for 205 and CONNECT, but it also\nmade HEAD responses drop the header unconditionally. RFC 9110 section 8.6\nallows a server to send Content-Length in a HEAD response, and RFC 9112\nsection 6.3 rule 1 exempts HEAD responses from body framing, so the header is\nmetadata there and cannot desync a connection. Suppressing it breaks the\ncommon case of using HEAD to learn the size of a resource without fetching it,\nand it is asymmetric: our own client parser honours a HEAD Content-Length, and\nthe HTTP/2 renderer never consulted contentLengthAllowed at all.\n\nModification:\nRestore the pre-1.4 predicate for HEAD (contentLengthAllowedForHead now\ndefers to StatusCode.allowsEntity) and add the length-based policy at render\ntime: a HEAD response renders Content-Length only when the entity declares a\nlength greater than zero. A zero length nearly always means the application\nhad no body to hand over rather than that the resource is empty, and the\nclient parser already only honours a HEAD Content-Length when it is greater\nthan zero. Chunked and CloseDelimited entities have no known length and are\nunchanged. 205, 204, 304 and CONNECT behaviour from #962 is untouched.\n\nResult:\nA HEAD response completed with HttpEntity.Strict or\nHttpEntity.Default(contentType, length, Source.empty) renders the declared\nContent-Length again, while an empty entity renders none.\n\nTests:\n- sbt \"http-core / Test / testOnly org.apache.pekko.http.impl.engine.rendering.ResponseRendererSpec org.apache.pekko.http.impl.engine.server.HttpServerSpec org.apache.pekko.http.scaladsl.model.HttpMethodsSpec\" - 110 passed\n- sbt \"http-core / Test / testOnly org.apache.pekko.http.impl.engine.client.HostConnectionPoolSpec\" - 66 passed\n- sbt +mimaReportBinaryIssues - success\n- scalafmt --mode diff-ref\u003dupstream/main - clean\n- git diff --check - clean\n- sbt \"http-core / test\" and sbt \"docs / paradox\" - not run to completion locally, left to CI\n\nReferences:\nFixes #1236, Refs #962"
    }
  ],
  "next": "6d8605a71fe11c803ade411ad71b0b7a4c5ac882"
}
