Skip to content

Replace per-byte partial-boundary scan with rfind lookbehind - #300

Merged
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data
Jun 4, 2026
Merged

Replace per-byte partial-boundary scan with rfind lookbehind#300
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data

Conversation

@Kludex

Copy link
Copy Markdown
Owner

What

When find cannot locate a whole boundary in part data, the tail may hold a partial boundary that completes in the next chunk. The old fallback seeded the per-byte matcher at the tail, which thrashed on CR/LF-dense bodies: \r\n matches the boundary prefix, fails at -, resets, reconsiders, and re-runs find for every \r\n pair near the chunk end.

Since self.boundary is \r\n-- + boundary and an RFC boundary cannot contain CR, the last CR in the tail is the only candidate prefix start. We anchor on it with rfind, confirm with startswith, carry the partial via index, and let the existing end-of-chunk flush emit the data and re-mark the lookbehind. This mirrors multer's lookbehind strategy.

Impact

Benchmarked against multipart_bench (1MB part body, 64KB chunks, sansio variant):

Scenariobeforeafter
worstcase_crlf~2700 MB/s~18500 MB/s (6.9x)
worstcase_lf~21500 MB/sunchanged
worstcase_bchar~16400 MB/sunchanged
file uploadunchangedunchanged

worstcase_crlf was the slowest scenario across all pure-Python parsers; it is now the fastest, reaching parity with the LF case.

Correctness

Verified behaviorally identical to the previous parser across 82k differential comparisons of the full callback-event stream and error type/offset: every chunk-split strategy (whole, byte-by-byte, fixed-size, random) plus an exhaustive two-chunk sweep with the boundary edge landing on every byte offset. Zero mismatches.

Adds a CRLF-dense corpus case (crlf_dense_part_data) to the data-driven test suite, run both whole-write and byte-by-byte. 100% coverage maintained.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

When `find` cannot locate a whole boundary in part data, the tail may
hold a partial boundary that completes in the next chunk. The old
fallback seeded the per-byte matcher at the tail, which thrashed on
CR/LF-dense bodies: `\r\n` matches the boundary prefix, fails at `-`,
resets, reconsiders, and re-runs `find` for every `\r\n` pair near the
chunk end.
Since `self.boundary` is `\r\n--` + boundary and an RFC boundary cannot
contain CR, the last CR in the tail is the only candidate prefix start.
Anchor on it with `rfind`, confirm with `startswith`, carry the partial
via `index`, and let the existing end-of-chunk flush emit the data and
re-mark the lookbehind. This mirrors multer's lookbehind strategy.
worstcase_crlf goes from ~2700 to ~18500 MB/s (6.9x), reaching parity
with the LF case; LF, boundary-char, and file-upload paths are
unchanged. Verified behaviorally identical to the previous parser across
82k differential comparisons (every chunk-split strategy incl.
byte-by-byte, with the boundary edge landing on every offset).
@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing speed-up-crlf-dense-part-data (12cbd2b) with main (4cffc68)

Open in CodSpeed

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@Kludex

Copy link
Copy Markdown
OwnerAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kludex
Kludex merged commit 8672979 into mainJun 4, 2026
15 checks passed
@Kludex
Kludex deleted the speed-up-crlf-dense-part-data branch June 4, 2026 15:42
@KludexKludex mentioned this pull request Jun 4, 2026
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Jun 29, 2026
This MR contains the following updates:
| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [debugpy](https://aka.ms/debugpy) ([source](https://github.com/microsoft/debugpy)) | dev | patch | `1.8.20` → `1.8.21` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microsoft/debugpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microsoft/debugpy) |
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.4.4` → `2.4.6` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) ([changelog](https://github.com/pydantic/pydantic-settings/releases)) | dependencies | patch | `2.14.0` → `2.14.2` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-settings/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-settings) |
| [python-multipart](https://github.com/Kludex/python-multipart) ([changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)) | dependencies | patch | `^0.0.22` → `^0.0.32` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/python-multipart/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/python-multipart) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.32.0.20240523` → `2.32.4.20260324` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
---
### Release Notes
<details>
<summary>microsoft/debugpy (debugpy)</summary>
### [`v1.8.21`](https://github.com/microsoft/debugpy/releases/tag/v1.8.21): debugpy v1.8.21
[Compare Source](microsoft/debugpy@v1.8.20...v1.8.21)
Fixes for:
- Return evaluate result in DAP response body instead of writing to stdout: [#&#8203;2027](microsoft/debugpy#2027)
- Prevent invalid `scopes` request from crashing debug session: [#&#8203;2026](microsoft/debugpy#2026)
- Skip uninitialized `__slots__` in variable resolver: [#&#8203;2024](microsoft/debugpy#2024)
- Handle `-c` arguments that are `bytes` instead of `str`: [#&#8203;2021](microsoft/debugpy#2021)
- Fix evaluation of variables from chained exception frames: [#&#8203;2018](microsoft/debugpy#2018)
- `ContinueRequest` with a specific `threadId` no longer resumes all threads (in-process adapter): [#&#8203;2012](microsoft/debugpy#2012)
- Avoid strong reference to exceptions during unwind: [#&#8203;2008](microsoft/debugpy#2008)
- Show error message on evaluate failures in the hover context: [#&#8203;2006](microsoft/debugpy#2006)
- Display `dlerror` output when `dlopen` fails: [#&#8203;2000](microsoft/debugpy#2000)
- Replace removed `pkgutil.get_loader` with `importlib.util.find_spec` in `get_fullname`: [#&#8203;1998](microsoft/debugpy#1998)
Enhancements:
- Add option to ignore all system exit codes: [#&#8203;2017](microsoft/debugpy#2017)
- Pull changes from pydevd up to March 2026: [#&#8203;2010](microsoft/debugpy#2010)
Infrastructure work:
- Suppress Flawfinder false positives on Cython memcpy / read-loop iterators (TSA [#&#8203;2816216](https://github.com/microsoft/debugpy/issues/2816216), [#&#8203;2816217](https://github.com/microsoft/debugpy/issues/2816217), [#&#8203;2816218](https://github.com/microsoft/debugpy/issues/2816218), [#&#8203;2816219](https://github.com/microsoft/debugpy/issues/2816219), [#&#8203;2816220](https://github.com/microsoft/debugpy/issues/2816220)): [#&#8203;2028](microsoft/debugpy#2028), [#&#8203;2029](microsoft/debugpy#2029), [#&#8203;2030](microsoft/debugpy#2030), [#&#8203;2031](microsoft/debugpy#2031), [#&#8203;2032](microsoft/debugpy#2032)
Thanks to [@&#8203;maxbachmann](https://github.com/maxbachmann), [@&#8203;mfussenegger](https://github.com/mfussenegger), and [@&#8203;sambrightman](https://github.com/sambrightman) for the commits.
</details>
<details>
<summary>numpy/numpy (numpy)</summary>
### [`v2.4.6`](https://github.com/numpy/numpy/releases/tag/v2.4.6): (May 18, 2026)
[Compare Source](numpy/numpy@v2.4.5...v2.4.6)
### NumPy 2.4.6 Release Notes
NumPy 2.4.6 is a quick release that fixes a regression discovered in the 2.4.5
release.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 4 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- !EarlMilktea
- Charles Harris
- Sebastian Berg
- Warren Weckesser
#### Pull requests merged
A total of 4 pull requests were merged for this release.
- [#&#8203;31444](numpy/numpy#31444): MAINT: Prepare 2.4.x for further development
- [#&#8203;31453](numpy/numpy#31453): BUG: Fix regression in `arr.conj()`
- [#&#8203;31459](numpy/numpy#31459): BUG: `np.linalg.svd(..., hermitian=True)` returns non-unitary...
- [#&#8203;31460](numpy/numpy#31460): BUG: Don't call INCREF/DECREF on descr in NpyStringAcquireAllocator...
### [`v2.4.5`](https://github.com/numpy/numpy/releases/tag/v2.4.5): (May 15, 2026)
[Compare Source](numpy/numpy@v2.4.4...v2.4.5)
### NumPy 2.4.5 Release Notes
NumPy 2.4.5 is a patch release that fixes bugs discovered after the 2.4.4
release, has some typing improvements, and maintains infrastructure.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 17 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- Aleksei Nikiforov
- Anarion Zuo +
- Ankit Ahlawat
- Breno Favaretto +
- Charles Harris
- Igor Krivenko +
- Ijtihed Kilani +
- Joren Hammudoglu
- Maarten Baert +
- Matti Picus
- Nathan Goldbaum
- Praneeth Kodumagulla +
- Ralf Gommers
- RoomWithOutRoof +
- Sebastian Berg
- Warren Weckesser
- div +
#### Pull requests merged
A total of 28 pull requests were merged for this release.
- [#&#8203;31093](numpy/numpy#31093): MAINT: Prepare 2.4.x for further development
- [#&#8203;31182](numpy/numpy#31182): TYP: fix `np.shape` assignability issue for python lists ([#&#8203;31171](numpy/numpy#31171))
- [#&#8203;31197](numpy/numpy#31197): ENH: Return rank 0 for empty matrices in matrix\_rank ([#&#8203;30422](numpy/numpy#30422))
- [#&#8203;31198](numpy/numpy#31198): CI/BUG: add native jobs for s390x, fix bug in `pack_inner`...
- [#&#8203;31199](numpy/numpy#31199): BUG: f2py map complex\_long\_double to NPY\_CLONGDOUBLE
- [#&#8203;31205](numpy/numpy#31205): MAINT: f2py: Stop setting re.\_MAXCACHE to 50.
- [#&#8203;31206](numpy/numpy#31206): BUG: fix heap buffer overflow in timedelta to string casts
- [#&#8203;31207](numpy/numpy#31207): MAINT: Rename ppc64le and s390x workflow ([#&#8203;31121](numpy/numpy#31121))
- [#&#8203;31208](numpy/numpy#31208): BUG: Fix matvec/vecmat in-place aliasing (out=input produces...
- [#&#8203;31209](numpy/numpy#31209): TYP: `tile`: accept numpy scalars and arrays as second argument...
- [#&#8203;31211](numpy/numpy#31211): DEP: Undo deprecation for np.dtype() signature used by old pickles...
- [#&#8203;31212](numpy/numpy#31212): REV: Manual revert of float16 svml use ([#&#8203;31178](numpy/numpy#31178))
- [#&#8203;31222](numpy/numpy#31222): TYP: `ix_` fix for boolean and non-1d input ([#&#8203;31218](numpy/numpy#31218))
- [#&#8203;31329](numpy/numpy#31329): BUG: incorrect temp elision for new-style (NEP 43) user-defined...
- [#&#8203;31330](numpy/numpy#31330): TYP: fix sliding\_window\_view axis parameter typing
- [#&#8203;31335](numpy/numpy#31335): BUG: Prevent deadlock due to downstream importing NumPy in dlopen...
- [#&#8203;31336](numpy/numpy#31336): BUG: Fix segfault in nditer.multi\_index when \_\_getitem\_\_ raises...
- [#&#8203;31338](numpy/numpy#31338): TYP: Fix ruff lint error
- [#&#8203;31357](numpy/numpy#31357): BUG: fix memory leak in np.zeros when fill-zero loop raises ([#&#8203;31320](numpy/numpy#31320))
- [#&#8203;31358](numpy/numpy#31358): BUG: np.einsum() fails with a 0-dimensional out argument and...
- [#&#8203;31379](numpy/numpy#31379): BUG: Fix signed overflow issue in npy\_gcd for INT\_MIN on s390x...
- [#&#8203;31383](numpy/numpy#31383): CI: remove Cirrus CI FreeBSD job ([#&#8203;31380](numpy/numpy#31380))
- [#&#8203;31390](numpy/numpy#31390): BUILD: newer MKL uses so.3
- [#&#8203;31391](numpy/numpy#31391): BLD/MAINT: improve support for Intel LLVM compilers
- [#&#8203;31401](numpy/numpy#31401): BUG: Avoid UB in [safe]()\[add,sub,mul] helpers ([#&#8203;31396](numpy/numpy#31396))
- [#&#8203;31402](numpy/numpy#31402): BUG: exclude \_\_pycache\_\_ directories from wheels ([#&#8203;31397](numpy/numpy#31397))
- [#&#8203;31404](numpy/numpy#31404): TYP: `_NestedSequence` type parameter default to work around...
- [#&#8203;31426](numpy/numpy#31426): TYP: Fix `DTypeLike` runtime type-checker support ([#&#8203;31425](numpy/numpy#31425))
</details>
<details>
<summary>pydantic/pydantic-settings (pydantic-settings)</summary>
### [`v2.14.2`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.2)
[Compare Source](pydantic/pydantic-settings@v2.14.1...v2.14.2)
#### What's Changed
This is a security patch release.
- Prevent `NestedSecretsSettingsSource` from following symlinks outside `secrets_dir` by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;889](pydantic/pydantic-settings#889)
- Prepare release 2.14.2 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;890](pydantic/pydantic-settings#890)
##### Security
Fixes [GHSA-4xgf-cpjx-pc3j](GHSA-4xgf-cpjx-pc3j): `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` could follow a symbolic link inside `secrets_dir` pointing outside it, reading out-of-tree files into settings values and bypassing the `secrets_dir_max_size` cap. Affected versions: `>= 2.12.0, < 2.14.2`.
**Full Changelog**: <pydantic/pydantic-settings@v2.14.1...v2.14.2>
### [`v2.14.1`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.1)
[Compare Source](pydantic/pydantic-settings@v2.14.0...v2.14.1)
#### What's Changed
- Bump the python-packages group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;850](pydantic/pydantic-settings#850)
- Bump the python-packages group with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;854](pydantic/pydantic-settings#854)
- Bump the github-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;853](pydantic/pydantic-settings#853)
- Bump the python-packages group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;856](pydantic/pydantic-settings#856)
- Fix field named `cls` conflicting with classmethod parameter by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;858](pydantic/pydantic-settings#858)
- Prepare release 2.14.1 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;859](pydantic/pydantic-settings#859)
**Full Changelog**: <pydantic/pydantic-settings@v2.14.0...v2.14.1>
</details>
<details>
<summary>Kludex/python-multipart (python-multipart)</summary>
### [`v0.0.32`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0032-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.31...0.0.32)
- Speed up partial-boundary scanning for CR/LF-dense part data [#&#8203;300](Kludex/python-multipart#300).
### [`v0.0.31`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0031-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.30...0.0.31)
- Speed up multipart header parsing and callback dispatch [#&#8203;295](Kludex/python-multipart#295).
- Bound header field name size before validating [#&#8203;296](Kludex/python-multipart#296).
- Validate `Content-Length` is non-negative in `parse_form` [#&#8203;297](Kludex/python-multipart#297).
### [`v0.0.30`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0030-2026-05-31)
[Compare Source](Kludex/python-multipart@0.0.29...0.0.30)
- Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator [#&#8203;290](Kludex/python-multipart#290).
- Ignore RFC 2231/5987 extended parameters (`name*`, `filename*`) in `parse_options_header`, keeping the plain parameter authoritative per [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) [#&#8203;291](Kludex/python-multipart#291).
### [`v0.0.29`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0029-2026-05-17)
[Compare Source](Kludex/python-multipart@0.0.28...0.0.29)
- Handle malformed RFC 2231 continuations in `parse_options_header` [#&#8203;270](Kludex/python-multipart#270).
### [`v0.0.28`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0028-2026-05-10)
[Compare Source](Kludex/python-multipart@0.0.27...0.0.28)
- Speed up partial-boundary tail scan via `bytes.find` [#&#8203;281](Kludex/python-multipart#281).
- Cap multipart boundary length at 256 bytes [#&#8203;282](Kludex/python-multipart#282).
### [`v0.0.27`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0027-2026-04-27)
[Compare Source](Kludex/python-multipart@0.0.26...0.0.27)
- Add multipart header limits [#&#8203;267](Kludex/python-multipart#267).
- Pass parse offsets via constructors [#&#8203;268](Kludex/python-multipart#268).
### [`v0.0.26`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0026-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.25...0.0.26)
- Skip preamble before the first multipart boundary more efficiently [#&#8203;262](Kludex/python-multipart#262).
- Silently discard epilogue data after the closing multipart boundary [#&#8203;259](Kludex/python-multipart#259).
### [`v0.0.25`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0025-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.24...0.0.25)
- Add MIME content type info to `File` [#&#8203;143](Kludex/python-multipart#143).
- Handle CTE values case-insensitively [#&#8203;258](Kludex/python-multipart#258).
- Remove custom `FormParser` classes [#&#8203;257](Kludex/python-multipart#257).
- Add `UPLOAD_DELETE_TMP` to `FormParser` config [#&#8203;254](Kludex/python-multipart#254).
- Emit `field_end` for trailing bare field names on finalize [#&#8203;230](Kludex/python-multipart#230).
- Handle multipart headers case-insensitively [#&#8203;252](Kludex/python-multipart#252).
- Apply Apache-2.0 properly [#&#8203;247](Kludex/python-multipart#247).
### [`v0.0.24`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0024-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.23...0.0.24)
- Validate `chunk_size` in `parse_form()` [#&#8203;244](Kludex/python-multipart#244).
### [`v0.0.23`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0023-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.22...0.0.23)
- Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#&#8203;196](Kludex/python-multipart#196).
- Return processed length from `QuerystringParser._internal_write` [#&#8203;229](Kludex/python-multipart#229).
- Cleanup metadata dunders from `__init__.py` [#&#8203;227](Kludex/python-multipart#227).
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box
---
This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTYiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNDYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->
See merge request swiss-armed-forces/cyber-command/cea/loom!460
Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Kludex
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Replace per-byte partial-boundary scan with rfind lookbehind by Kludex · Pull Request #300 · Kludex/python-multipart · GitHub
Skip to content

Replace per-byte partial-boundary scan with rfind lookbehind - #300

Merged
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data
Jun 4, 2026
Merged

Replace per-byte partial-boundary scan with rfind lookbehind#300
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data

Conversation

@Kludex

Copy link
Copy Markdown
Owner

What

When find cannot locate a whole boundary in part data, the tail may hold a partial boundary that completes in the next chunk. The old fallback seeded the per-byte matcher at the tail, which thrashed on CR/LF-dense bodies: \r\n matches the boundary prefix, fails at -, resets, reconsiders, and re-runs find for every \r\n pair near the chunk end.

Since self.boundary is \r\n-- + boundary and an RFC boundary cannot contain CR, the last CR in the tail is the only candidate prefix start. We anchor on it with rfind, confirm with startswith, carry the partial via index, and let the existing end-of-chunk flush emit the data and re-mark the lookbehind. This mirrors multer's lookbehind strategy.

Impact

Benchmarked against multipart_bench (1MB part body, 64KB chunks, sansio variant):

Scenariobeforeafter
worstcase_crlf~2700 MB/s~18500 MB/s (6.9x)
worstcase_lf~21500 MB/sunchanged
worstcase_bchar~16400 MB/sunchanged
file uploadunchangedunchanged

worstcase_crlf was the slowest scenario across all pure-Python parsers; it is now the fastest, reaching parity with the LF case.

Correctness

Verified behaviorally identical to the previous parser across 82k differential comparisons of the full callback-event stream and error type/offset: every chunk-split strategy (whole, byte-by-byte, fixed-size, random) plus an exhaustive two-chunk sweep with the boundary edge landing on every byte offset. Zero mismatches.

Adds a CRLF-dense corpus case (crlf_dense_part_data) to the data-driven test suite, run both whole-write and byte-by-byte. 100% coverage maintained.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

When `find` cannot locate a whole boundary in part data, the tail may
hold a partial boundary that completes in the next chunk. The old
fallback seeded the per-byte matcher at the tail, which thrashed on
CR/LF-dense bodies: `\r\n` matches the boundary prefix, fails at `-`,
resets, reconsiders, and re-runs `find` for every `\r\n` pair near the
chunk end.
Since `self.boundary` is `\r\n--` + boundary and an RFC boundary cannot
contain CR, the last CR in the tail is the only candidate prefix start.
Anchor on it with `rfind`, confirm with `startswith`, carry the partial
via `index`, and let the existing end-of-chunk flush emit the data and
re-mark the lookbehind. This mirrors multer's lookbehind strategy.
worstcase_crlf goes from ~2700 to ~18500 MB/s (6.9x), reaching parity
with the LF case; LF, boundary-char, and file-upload paths are
unchanged. Verified behaviorally identical to the previous parser across
82k differential comparisons (every chunk-split strategy incl.
byte-by-byte, with the boundary edge landing on every offset).
@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing speed-up-crlf-dense-part-data (12cbd2b) with main (4cffc68)

Open in CodSpeed

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@Kludex

Copy link
Copy Markdown
OwnerAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kludex
Kludex merged commit 8672979 into mainJun 4, 2026
15 checks passed
@Kludex
Kludex deleted the speed-up-crlf-dense-part-data branch June 4, 2026 15:42
@KludexKludex mentioned this pull request Jun 4, 2026
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Jun 29, 2026
This MR contains the following updates:
| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [debugpy](https://aka.ms/debugpy) ([source](https://github.com/microsoft/debugpy)) | dev | patch | `1.8.20` → `1.8.21` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microsoft/debugpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microsoft/debugpy) |
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.4.4` → `2.4.6` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) ([changelog](https://github.com/pydantic/pydantic-settings/releases)) | dependencies | patch | `2.14.0` → `2.14.2` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-settings/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-settings) |
| [python-multipart](https://github.com/Kludex/python-multipart) ([changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)) | dependencies | patch | `^0.0.22` → `^0.0.32` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/python-multipart/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/python-multipart) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.32.0.20240523` → `2.32.4.20260324` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
---
### Release Notes
<details>
<summary>microsoft/debugpy (debugpy)</summary>
### [`v1.8.21`](https://github.com/microsoft/debugpy/releases/tag/v1.8.21): debugpy v1.8.21
[Compare Source](microsoft/debugpy@v1.8.20...v1.8.21)
Fixes for:
- Return evaluate result in DAP response body instead of writing to stdout: [#&#8203;2027](microsoft/debugpy#2027)
- Prevent invalid `scopes` request from crashing debug session: [#&#8203;2026](microsoft/debugpy#2026)
- Skip uninitialized `__slots__` in variable resolver: [#&#8203;2024](microsoft/debugpy#2024)
- Handle `-c` arguments that are `bytes` instead of `str`: [#&#8203;2021](microsoft/debugpy#2021)
- Fix evaluation of variables from chained exception frames: [#&#8203;2018](microsoft/debugpy#2018)
- `ContinueRequest` with a specific `threadId` no longer resumes all threads (in-process adapter): [#&#8203;2012](microsoft/debugpy#2012)
- Avoid strong reference to exceptions during unwind: [#&#8203;2008](microsoft/debugpy#2008)
- Show error message on evaluate failures in the hover context: [#&#8203;2006](microsoft/debugpy#2006)
- Display `dlerror` output when `dlopen` fails: [#&#8203;2000](microsoft/debugpy#2000)
- Replace removed `pkgutil.get_loader` with `importlib.util.find_spec` in `get_fullname`: [#&#8203;1998](microsoft/debugpy#1998)
Enhancements:
- Add option to ignore all system exit codes: [#&#8203;2017](microsoft/debugpy#2017)
- Pull changes from pydevd up to March 2026: [#&#8203;2010](microsoft/debugpy#2010)
Infrastructure work:
- Suppress Flawfinder false positives on Cython memcpy / read-loop iterators (TSA [#&#8203;2816216](https://github.com/microsoft/debugpy/issues/2816216), [#&#8203;2816217](https://github.com/microsoft/debugpy/issues/2816217), [#&#8203;2816218](https://github.com/microsoft/debugpy/issues/2816218), [#&#8203;2816219](https://github.com/microsoft/debugpy/issues/2816219), [#&#8203;2816220](https://github.com/microsoft/debugpy/issues/2816220)): [#&#8203;2028](microsoft/debugpy#2028), [#&#8203;2029](microsoft/debugpy#2029), [#&#8203;2030](microsoft/debugpy#2030), [#&#8203;2031](microsoft/debugpy#2031), [#&#8203;2032](microsoft/debugpy#2032)
Thanks to [@&#8203;maxbachmann](https://github.com/maxbachmann), [@&#8203;mfussenegger](https://github.com/mfussenegger), and [@&#8203;sambrightman](https://github.com/sambrightman) for the commits.
</details>
<details>
<summary>numpy/numpy (numpy)</summary>
### [`v2.4.6`](https://github.com/numpy/numpy/releases/tag/v2.4.6): (May 18, 2026)
[Compare Source](numpy/numpy@v2.4.5...v2.4.6)
### NumPy 2.4.6 Release Notes
NumPy 2.4.6 is a quick release that fixes a regression discovered in the 2.4.5
release.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 4 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- !EarlMilktea
- Charles Harris
- Sebastian Berg
- Warren Weckesser
#### Pull requests merged
A total of 4 pull requests were merged for this release.
- [#&#8203;31444](numpy/numpy#31444): MAINT: Prepare 2.4.x for further development
- [#&#8203;31453](numpy/numpy#31453): BUG: Fix regression in `arr.conj()`
- [#&#8203;31459](numpy/numpy#31459): BUG: `np.linalg.svd(..., hermitian=True)` returns non-unitary...
- [#&#8203;31460](numpy/numpy#31460): BUG: Don't call INCREF/DECREF on descr in NpyStringAcquireAllocator...
### [`v2.4.5`](https://github.com/numpy/numpy/releases/tag/v2.4.5): (May 15, 2026)
[Compare Source](numpy/numpy@v2.4.4...v2.4.5)
### NumPy 2.4.5 Release Notes
NumPy 2.4.5 is a patch release that fixes bugs discovered after the 2.4.4
release, has some typing improvements, and maintains infrastructure.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 17 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- Aleksei Nikiforov
- Anarion Zuo +
- Ankit Ahlawat
- Breno Favaretto +
- Charles Harris
- Igor Krivenko +
- Ijtihed Kilani +
- Joren Hammudoglu
- Maarten Baert +
- Matti Picus
- Nathan Goldbaum
- Praneeth Kodumagulla +
- Ralf Gommers
- RoomWithOutRoof +
- Sebastian Berg
- Warren Weckesser
- div +
#### Pull requests merged
A total of 28 pull requests were merged for this release.
- [#&#8203;31093](numpy/numpy#31093): MAINT: Prepare 2.4.x for further development
- [#&#8203;31182](numpy/numpy#31182): TYP: fix `np.shape` assignability issue for python lists ([#&#8203;31171](numpy/numpy#31171))
- [#&#8203;31197](numpy/numpy#31197): ENH: Return rank 0 for empty matrices in matrix\_rank ([#&#8203;30422](numpy/numpy#30422))
- [#&#8203;31198](numpy/numpy#31198): CI/BUG: add native jobs for s390x, fix bug in `pack_inner`...
- [#&#8203;31199](numpy/numpy#31199): BUG: f2py map complex\_long\_double to NPY\_CLONGDOUBLE
- [#&#8203;31205](numpy/numpy#31205): MAINT: f2py: Stop setting re.\_MAXCACHE to 50.
- [#&#8203;31206](numpy/numpy#31206): BUG: fix heap buffer overflow in timedelta to string casts
- [#&#8203;31207](numpy/numpy#31207): MAINT: Rename ppc64le and s390x workflow ([#&#8203;31121](numpy/numpy#31121))
- [#&#8203;31208](numpy/numpy#31208): BUG: Fix matvec/vecmat in-place aliasing (out=input produces...
- [#&#8203;31209](numpy/numpy#31209): TYP: `tile`: accept numpy scalars and arrays as second argument...
- [#&#8203;31211](numpy/numpy#31211): DEP: Undo deprecation for np.dtype() signature used by old pickles...
- [#&#8203;31212](numpy/numpy#31212): REV: Manual revert of float16 svml use ([#&#8203;31178](numpy/numpy#31178))
- [#&#8203;31222](numpy/numpy#31222): TYP: `ix_` fix for boolean and non-1d input ([#&#8203;31218](numpy/numpy#31218))
- [#&#8203;31329](numpy/numpy#31329): BUG: incorrect temp elision for new-style (NEP 43) user-defined...
- [#&#8203;31330](numpy/numpy#31330): TYP: fix sliding\_window\_view axis parameter typing
- [#&#8203;31335](numpy/numpy#31335): BUG: Prevent deadlock due to downstream importing NumPy in dlopen...
- [#&#8203;31336](numpy/numpy#31336): BUG: Fix segfault in nditer.multi\_index when \_\_getitem\_\_ raises...
- [#&#8203;31338](numpy/numpy#31338): TYP: Fix ruff lint error
- [#&#8203;31357](numpy/numpy#31357): BUG: fix memory leak in np.zeros when fill-zero loop raises ([#&#8203;31320](numpy/numpy#31320))
- [#&#8203;31358](numpy/numpy#31358): BUG: np.einsum() fails with a 0-dimensional out argument and...
- [#&#8203;31379](numpy/numpy#31379): BUG: Fix signed overflow issue in npy\_gcd for INT\_MIN on s390x...
- [#&#8203;31383](numpy/numpy#31383): CI: remove Cirrus CI FreeBSD job ([#&#8203;31380](numpy/numpy#31380))
- [#&#8203;31390](numpy/numpy#31390): BUILD: newer MKL uses so.3
- [#&#8203;31391](numpy/numpy#31391): BLD/MAINT: improve support for Intel LLVM compilers
- [#&#8203;31401](numpy/numpy#31401): BUG: Avoid UB in [safe]()\[add,sub,mul] helpers ([#&#8203;31396](numpy/numpy#31396))
- [#&#8203;31402](numpy/numpy#31402): BUG: exclude \_\_pycache\_\_ directories from wheels ([#&#8203;31397](numpy/numpy#31397))
- [#&#8203;31404](numpy/numpy#31404): TYP: `_NestedSequence` type parameter default to work around...
- [#&#8203;31426](numpy/numpy#31426): TYP: Fix `DTypeLike` runtime type-checker support ([#&#8203;31425](numpy/numpy#31425))
</details>
<details>
<summary>pydantic/pydantic-settings (pydantic-settings)</summary>
### [`v2.14.2`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.2)
[Compare Source](pydantic/pydantic-settings@v2.14.1...v2.14.2)
#### What's Changed
This is a security patch release.
- Prevent `NestedSecretsSettingsSource` from following symlinks outside `secrets_dir` by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;889](pydantic/pydantic-settings#889)
- Prepare release 2.14.2 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;890](pydantic/pydantic-settings#890)
##### Security
Fixes [GHSA-4xgf-cpjx-pc3j](GHSA-4xgf-cpjx-pc3j): `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` could follow a symbolic link inside `secrets_dir` pointing outside it, reading out-of-tree files into settings values and bypassing the `secrets_dir_max_size` cap. Affected versions: `>= 2.12.0, < 2.14.2`.
**Full Changelog**: <pydantic/pydantic-settings@v2.14.1...v2.14.2>
### [`v2.14.1`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.1)
[Compare Source](pydantic/pydantic-settings@v2.14.0...v2.14.1)
#### What's Changed
- Bump the python-packages group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;850](pydantic/pydantic-settings#850)
- Bump the python-packages group with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;854](pydantic/pydantic-settings#854)
- Bump the github-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;853](pydantic/pydantic-settings#853)
- Bump the python-packages group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;856](pydantic/pydantic-settings#856)
- Fix field named `cls` conflicting with classmethod parameter by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;858](pydantic/pydantic-settings#858)
- Prepare release 2.14.1 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;859](pydantic/pydantic-settings#859)
**Full Changelog**: <pydantic/pydantic-settings@v2.14.0...v2.14.1>
</details>
<details>
<summary>Kludex/python-multipart (python-multipart)</summary>
### [`v0.0.32`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0032-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.31...0.0.32)
- Speed up partial-boundary scanning for CR/LF-dense part data [#&#8203;300](Kludex/python-multipart#300).
### [`v0.0.31`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0031-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.30...0.0.31)
- Speed up multipart header parsing and callback dispatch [#&#8203;295](Kludex/python-multipart#295).
- Bound header field name size before validating [#&#8203;296](Kludex/python-multipart#296).
- Validate `Content-Length` is non-negative in `parse_form` [#&#8203;297](Kludex/python-multipart#297).
### [`v0.0.30`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0030-2026-05-31)
[Compare Source](Kludex/python-multipart@0.0.29...0.0.30)
- Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator [#&#8203;290](Kludex/python-multipart#290).
- Ignore RFC 2231/5987 extended parameters (`name*`, `filename*`) in `parse_options_header`, keeping the plain parameter authoritative per [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) [#&#8203;291](Kludex/python-multipart#291).
### [`v0.0.29`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0029-2026-05-17)
[Compare Source](Kludex/python-multipart@0.0.28...0.0.29)
- Handle malformed RFC 2231 continuations in `parse_options_header` [#&#8203;270](Kludex/python-multipart#270).
### [`v0.0.28`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0028-2026-05-10)
[Compare Source](Kludex/python-multipart@0.0.27...0.0.28)
- Speed up partial-boundary tail scan via `bytes.find` [#&#8203;281](Kludex/python-multipart#281).
- Cap multipart boundary length at 256 bytes [#&#8203;282](Kludex/python-multipart#282).
### [`v0.0.27`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0027-2026-04-27)
[Compare Source](Kludex/python-multipart@0.0.26...0.0.27)
- Add multipart header limits [#&#8203;267](Kludex/python-multipart#267).
- Pass parse offsets via constructors [#&#8203;268](Kludex/python-multipart#268).
### [`v0.0.26`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0026-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.25...0.0.26)
- Skip preamble before the first multipart boundary more efficiently [#&#8203;262](Kludex/python-multipart#262).
- Silently discard epilogue data after the closing multipart boundary [#&#8203;259](Kludex/python-multipart#259).
### [`v0.0.25`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0025-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.24...0.0.25)
- Add MIME content type info to `File` [#&#8203;143](Kludex/python-multipart#143).
- Handle CTE values case-insensitively [#&#8203;258](Kludex/python-multipart#258).
- Remove custom `FormParser` classes [#&#8203;257](Kludex/python-multipart#257).
- Add `UPLOAD_DELETE_TMP` to `FormParser` config [#&#8203;254](Kludex/python-multipart#254).
- Emit `field_end` for trailing bare field names on finalize [#&#8203;230](Kludex/python-multipart#230).
- Handle multipart headers case-insensitively [#&#8203;252](Kludex/python-multipart#252).
- Apply Apache-2.0 properly [#&#8203;247](Kludex/python-multipart#247).
### [`v0.0.24`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0024-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.23...0.0.24)
- Validate `chunk_size` in `parse_form()` [#&#8203;244](Kludex/python-multipart#244).
### [`v0.0.23`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0023-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.22...0.0.23)
- Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#&#8203;196](Kludex/python-multipart#196).
- Return processed length from `QuerystringParser._internal_write` [#&#8203;229](Kludex/python-multipart#229).
- Cleanup metadata dunders from `__init__.py` [#&#8203;227](Kludex/python-multipart#227).
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box
---
This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTYiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNDYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->
See merge request swiss-armed-forces/cyber-command/cea/loom!460
Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Kludex
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Replace per-byte partial-boundary scan with rfind lookbehind by Kludex · Pull Request #300 · Kludex/python-multipart · GitHub
Skip to content

Replace per-byte partial-boundary scan with rfind lookbehind - #300

Merged
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data
Jun 4, 2026
Merged

Replace per-byte partial-boundary scan with rfind lookbehind#300
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data

Conversation

@Kludex

Copy link
Copy Markdown
Owner

What

When find cannot locate a whole boundary in part data, the tail may hold a partial boundary that completes in the next chunk. The old fallback seeded the per-byte matcher at the tail, which thrashed on CR/LF-dense bodies: \r\n matches the boundary prefix, fails at -, resets, reconsiders, and re-runs find for every \r\n pair near the chunk end.

Since self.boundary is \r\n-- + boundary and an RFC boundary cannot contain CR, the last CR in the tail is the only candidate prefix start. We anchor on it with rfind, confirm with startswith, carry the partial via index, and let the existing end-of-chunk flush emit the data and re-mark the lookbehind. This mirrors multer's lookbehind strategy.

Impact

Benchmarked against multipart_bench (1MB part body, 64KB chunks, sansio variant):

Scenariobeforeafter
worstcase_crlf~2700 MB/s~18500 MB/s (6.9x)
worstcase_lf~21500 MB/sunchanged
worstcase_bchar~16400 MB/sunchanged
file uploadunchangedunchanged

worstcase_crlf was the slowest scenario across all pure-Python parsers; it is now the fastest, reaching parity with the LF case.

Correctness

Verified behaviorally identical to the previous parser across 82k differential comparisons of the full callback-event stream and error type/offset: every chunk-split strategy (whole, byte-by-byte, fixed-size, random) plus an exhaustive two-chunk sweep with the boundary edge landing on every byte offset. Zero mismatches.

Adds a CRLF-dense corpus case (crlf_dense_part_data) to the data-driven test suite, run both whole-write and byte-by-byte. 100% coverage maintained.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

When `find` cannot locate a whole boundary in part data, the tail may
hold a partial boundary that completes in the next chunk. The old
fallback seeded the per-byte matcher at the tail, which thrashed on
CR/LF-dense bodies: `\r\n` matches the boundary prefix, fails at `-`,
resets, reconsiders, and re-runs `find` for every `\r\n` pair near the
chunk end.
Since `self.boundary` is `\r\n--` + boundary and an RFC boundary cannot
contain CR, the last CR in the tail is the only candidate prefix start.
Anchor on it with `rfind`, confirm with `startswith`, carry the partial
via `index`, and let the existing end-of-chunk flush emit the data and
re-mark the lookbehind. This mirrors multer's lookbehind strategy.
worstcase_crlf goes from ~2700 to ~18500 MB/s (6.9x), reaching parity
with the LF case; LF, boundary-char, and file-upload paths are
unchanged. Verified behaviorally identical to the previous parser across
82k differential comparisons (every chunk-split strategy incl.
byte-by-byte, with the boundary edge landing on every offset).
@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing speed-up-crlf-dense-part-data (12cbd2b) with main (4cffc68)

Open in CodSpeed

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@Kludex

Copy link
Copy Markdown
OwnerAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kludex
Kludex merged commit 8672979 into mainJun 4, 2026
15 checks passed
@Kludex
Kludex deleted the speed-up-crlf-dense-part-data branch June 4, 2026 15:42
@KludexKludex mentioned this pull request Jun 4, 2026
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Jun 29, 2026
This MR contains the following updates:
| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [debugpy](https://aka.ms/debugpy) ([source](https://github.com/microsoft/debugpy)) | dev | patch | `1.8.20` → `1.8.21` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microsoft/debugpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microsoft/debugpy) |
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.4.4` → `2.4.6` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) ([changelog](https://github.com/pydantic/pydantic-settings/releases)) | dependencies | patch | `2.14.0` → `2.14.2` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-settings/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-settings) |
| [python-multipart](https://github.com/Kludex/python-multipart) ([changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)) | dependencies | patch | `^0.0.22` → `^0.0.32` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/python-multipart/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/python-multipart) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.32.0.20240523` → `2.32.4.20260324` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
---
### Release Notes
<details>
<summary>microsoft/debugpy (debugpy)</summary>
### [`v1.8.21`](https://github.com/microsoft/debugpy/releases/tag/v1.8.21): debugpy v1.8.21
[Compare Source](microsoft/debugpy@v1.8.20...v1.8.21)
Fixes for:
- Return evaluate result in DAP response body instead of writing to stdout: [#&#8203;2027](microsoft/debugpy#2027)
- Prevent invalid `scopes` request from crashing debug session: [#&#8203;2026](microsoft/debugpy#2026)
- Skip uninitialized `__slots__` in variable resolver: [#&#8203;2024](microsoft/debugpy#2024)
- Handle `-c` arguments that are `bytes` instead of `str`: [#&#8203;2021](microsoft/debugpy#2021)
- Fix evaluation of variables from chained exception frames: [#&#8203;2018](microsoft/debugpy#2018)
- `ContinueRequest` with a specific `threadId` no longer resumes all threads (in-process adapter): [#&#8203;2012](microsoft/debugpy#2012)
- Avoid strong reference to exceptions during unwind: [#&#8203;2008](microsoft/debugpy#2008)
- Show error message on evaluate failures in the hover context: [#&#8203;2006](microsoft/debugpy#2006)
- Display `dlerror` output when `dlopen` fails: [#&#8203;2000](microsoft/debugpy#2000)
- Replace removed `pkgutil.get_loader` with `importlib.util.find_spec` in `get_fullname`: [#&#8203;1998](microsoft/debugpy#1998)
Enhancements:
- Add option to ignore all system exit codes: [#&#8203;2017](microsoft/debugpy#2017)
- Pull changes from pydevd up to March 2026: [#&#8203;2010](microsoft/debugpy#2010)
Infrastructure work:
- Suppress Flawfinder false positives on Cython memcpy / read-loop iterators (TSA [#&#8203;2816216](https://github.com/microsoft/debugpy/issues/2816216), [#&#8203;2816217](https://github.com/microsoft/debugpy/issues/2816217), [#&#8203;2816218](https://github.com/microsoft/debugpy/issues/2816218), [#&#8203;2816219](https://github.com/microsoft/debugpy/issues/2816219), [#&#8203;2816220](https://github.com/microsoft/debugpy/issues/2816220)): [#&#8203;2028](microsoft/debugpy#2028), [#&#8203;2029](microsoft/debugpy#2029), [#&#8203;2030](microsoft/debugpy#2030), [#&#8203;2031](microsoft/debugpy#2031), [#&#8203;2032](microsoft/debugpy#2032)
Thanks to [@&#8203;maxbachmann](https://github.com/maxbachmann), [@&#8203;mfussenegger](https://github.com/mfussenegger), and [@&#8203;sambrightman](https://github.com/sambrightman) for the commits.
</details>
<details>
<summary>numpy/numpy (numpy)</summary>
### [`v2.4.6`](https://github.com/numpy/numpy/releases/tag/v2.4.6): (May 18, 2026)
[Compare Source](numpy/numpy@v2.4.5...v2.4.6)
### NumPy 2.4.6 Release Notes
NumPy 2.4.6 is a quick release that fixes a regression discovered in the 2.4.5
release.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 4 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- !EarlMilktea
- Charles Harris
- Sebastian Berg
- Warren Weckesser
#### Pull requests merged
A total of 4 pull requests were merged for this release.
- [#&#8203;31444](numpy/numpy#31444): MAINT: Prepare 2.4.x for further development
- [#&#8203;31453](numpy/numpy#31453): BUG: Fix regression in `arr.conj()`
- [#&#8203;31459](numpy/numpy#31459): BUG: `np.linalg.svd(..., hermitian=True)` returns non-unitary...
- [#&#8203;31460](numpy/numpy#31460): BUG: Don't call INCREF/DECREF on descr in NpyStringAcquireAllocator...
### [`v2.4.5`](https://github.com/numpy/numpy/releases/tag/v2.4.5): (May 15, 2026)
[Compare Source](numpy/numpy@v2.4.4...v2.4.5)
### NumPy 2.4.5 Release Notes
NumPy 2.4.5 is a patch release that fixes bugs discovered after the 2.4.4
release, has some typing improvements, and maintains infrastructure.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 17 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- Aleksei Nikiforov
- Anarion Zuo +
- Ankit Ahlawat
- Breno Favaretto +
- Charles Harris
- Igor Krivenko +
- Ijtihed Kilani +
- Joren Hammudoglu
- Maarten Baert +
- Matti Picus
- Nathan Goldbaum
- Praneeth Kodumagulla +
- Ralf Gommers
- RoomWithOutRoof +
- Sebastian Berg
- Warren Weckesser
- div +
#### Pull requests merged
A total of 28 pull requests were merged for this release.
- [#&#8203;31093](numpy/numpy#31093): MAINT: Prepare 2.4.x for further development
- [#&#8203;31182](numpy/numpy#31182): TYP: fix `np.shape` assignability issue for python lists ([#&#8203;31171](numpy/numpy#31171))
- [#&#8203;31197](numpy/numpy#31197): ENH: Return rank 0 for empty matrices in matrix\_rank ([#&#8203;30422](numpy/numpy#30422))
- [#&#8203;31198](numpy/numpy#31198): CI/BUG: add native jobs for s390x, fix bug in `pack_inner`...
- [#&#8203;31199](numpy/numpy#31199): BUG: f2py map complex\_long\_double to NPY\_CLONGDOUBLE
- [#&#8203;31205](numpy/numpy#31205): MAINT: f2py: Stop setting re.\_MAXCACHE to 50.
- [#&#8203;31206](numpy/numpy#31206): BUG: fix heap buffer overflow in timedelta to string casts
- [#&#8203;31207](numpy/numpy#31207): MAINT: Rename ppc64le and s390x workflow ([#&#8203;31121](numpy/numpy#31121))
- [#&#8203;31208](numpy/numpy#31208): BUG: Fix matvec/vecmat in-place aliasing (out=input produces...
- [#&#8203;31209](numpy/numpy#31209): TYP: `tile`: accept numpy scalars and arrays as second argument...
- [#&#8203;31211](numpy/numpy#31211): DEP: Undo deprecation for np.dtype() signature used by old pickles...
- [#&#8203;31212](numpy/numpy#31212): REV: Manual revert of float16 svml use ([#&#8203;31178](numpy/numpy#31178))
- [#&#8203;31222](numpy/numpy#31222): TYP: `ix_` fix for boolean and non-1d input ([#&#8203;31218](numpy/numpy#31218))
- [#&#8203;31329](numpy/numpy#31329): BUG: incorrect temp elision for new-style (NEP 43) user-defined...
- [#&#8203;31330](numpy/numpy#31330): TYP: fix sliding\_window\_view axis parameter typing
- [#&#8203;31335](numpy/numpy#31335): BUG: Prevent deadlock due to downstream importing NumPy in dlopen...
- [#&#8203;31336](numpy/numpy#31336): BUG: Fix segfault in nditer.multi\_index when \_\_getitem\_\_ raises...
- [#&#8203;31338](numpy/numpy#31338): TYP: Fix ruff lint error
- [#&#8203;31357](numpy/numpy#31357): BUG: fix memory leak in np.zeros when fill-zero loop raises ([#&#8203;31320](numpy/numpy#31320))
- [#&#8203;31358](numpy/numpy#31358): BUG: np.einsum() fails with a 0-dimensional out argument and...
- [#&#8203;31379](numpy/numpy#31379): BUG: Fix signed overflow issue in npy\_gcd for INT\_MIN on s390x...
- [#&#8203;31383](numpy/numpy#31383): CI: remove Cirrus CI FreeBSD job ([#&#8203;31380](numpy/numpy#31380))
- [#&#8203;31390](numpy/numpy#31390): BUILD: newer MKL uses so.3
- [#&#8203;31391](numpy/numpy#31391): BLD/MAINT: improve support for Intel LLVM compilers
- [#&#8203;31401](numpy/numpy#31401): BUG: Avoid UB in [safe]()\[add,sub,mul] helpers ([#&#8203;31396](numpy/numpy#31396))
- [#&#8203;31402](numpy/numpy#31402): BUG: exclude \_\_pycache\_\_ directories from wheels ([#&#8203;31397](numpy/numpy#31397))
- [#&#8203;31404](numpy/numpy#31404): TYP: `_NestedSequence` type parameter default to work around...
- [#&#8203;31426](numpy/numpy#31426): TYP: Fix `DTypeLike` runtime type-checker support ([#&#8203;31425](numpy/numpy#31425))
</details>
<details>
<summary>pydantic/pydantic-settings (pydantic-settings)</summary>
### [`v2.14.2`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.2)
[Compare Source](pydantic/pydantic-settings@v2.14.1...v2.14.2)
#### What's Changed
This is a security patch release.
- Prevent `NestedSecretsSettingsSource` from following symlinks outside `secrets_dir` by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;889](pydantic/pydantic-settings#889)
- Prepare release 2.14.2 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;890](pydantic/pydantic-settings#890)
##### Security
Fixes [GHSA-4xgf-cpjx-pc3j](GHSA-4xgf-cpjx-pc3j): `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` could follow a symbolic link inside `secrets_dir` pointing outside it, reading out-of-tree files into settings values and bypassing the `secrets_dir_max_size` cap. Affected versions: `>= 2.12.0, < 2.14.2`.
**Full Changelog**: <pydantic/pydantic-settings@v2.14.1...v2.14.2>
### [`v2.14.1`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.1)
[Compare Source](pydantic/pydantic-settings@v2.14.0...v2.14.1)
#### What's Changed
- Bump the python-packages group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;850](pydantic/pydantic-settings#850)
- Bump the python-packages group with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;854](pydantic/pydantic-settings#854)
- Bump the github-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;853](pydantic/pydantic-settings#853)
- Bump the python-packages group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;856](pydantic/pydantic-settings#856)
- Fix field named `cls` conflicting with classmethod parameter by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;858](pydantic/pydantic-settings#858)
- Prepare release 2.14.1 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;859](pydantic/pydantic-settings#859)
**Full Changelog**: <pydantic/pydantic-settings@v2.14.0...v2.14.1>
</details>
<details>
<summary>Kludex/python-multipart (python-multipart)</summary>
### [`v0.0.32`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0032-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.31...0.0.32)
- Speed up partial-boundary scanning for CR/LF-dense part data [#&#8203;300](Kludex/python-multipart#300).
### [`v0.0.31`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0031-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.30...0.0.31)
- Speed up multipart header parsing and callback dispatch [#&#8203;295](Kludex/python-multipart#295).
- Bound header field name size before validating [#&#8203;296](Kludex/python-multipart#296).
- Validate `Content-Length` is non-negative in `parse_form` [#&#8203;297](Kludex/python-multipart#297).
### [`v0.0.30`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0030-2026-05-31)
[Compare Source](Kludex/python-multipart@0.0.29...0.0.30)
- Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator [#&#8203;290](Kludex/python-multipart#290).
- Ignore RFC 2231/5987 extended parameters (`name*`, `filename*`) in `parse_options_header`, keeping the plain parameter authoritative per [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) [#&#8203;291](Kludex/python-multipart#291).
### [`v0.0.29`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0029-2026-05-17)
[Compare Source](Kludex/python-multipart@0.0.28...0.0.29)
- Handle malformed RFC 2231 continuations in `parse_options_header` [#&#8203;270](Kludex/python-multipart#270).
### [`v0.0.28`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0028-2026-05-10)
[Compare Source](Kludex/python-multipart@0.0.27...0.0.28)
- Speed up partial-boundary tail scan via `bytes.find` [#&#8203;281](Kludex/python-multipart#281).
- Cap multipart boundary length at 256 bytes [#&#8203;282](Kludex/python-multipart#282).
### [`v0.0.27`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0027-2026-04-27)
[Compare Source](Kludex/python-multipart@0.0.26...0.0.27)
- Add multipart header limits [#&#8203;267](Kludex/python-multipart#267).
- Pass parse offsets via constructors [#&#8203;268](Kludex/python-multipart#268).
### [`v0.0.26`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0026-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.25...0.0.26)
- Skip preamble before the first multipart boundary more efficiently [#&#8203;262](Kludex/python-multipart#262).
- Silently discard epilogue data after the closing multipart boundary [#&#8203;259](Kludex/python-multipart#259).
### [`v0.0.25`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0025-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.24...0.0.25)
- Add MIME content type info to `File` [#&#8203;143](Kludex/python-multipart#143).
- Handle CTE values case-insensitively [#&#8203;258](Kludex/python-multipart#258).
- Remove custom `FormParser` classes [#&#8203;257](Kludex/python-multipart#257).
- Add `UPLOAD_DELETE_TMP` to `FormParser` config [#&#8203;254](Kludex/python-multipart#254).
- Emit `field_end` for trailing bare field names on finalize [#&#8203;230](Kludex/python-multipart#230).
- Handle multipart headers case-insensitively [#&#8203;252](Kludex/python-multipart#252).
- Apply Apache-2.0 properly [#&#8203;247](Kludex/python-multipart#247).
### [`v0.0.24`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0024-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.23...0.0.24)
- Validate `chunk_size` in `parse_form()` [#&#8203;244](Kludex/python-multipart#244).
### [`v0.0.23`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0023-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.22...0.0.23)
- Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#&#8203;196](Kludex/python-multipart#196).
- Return processed length from `QuerystringParser._internal_write` [#&#8203;229](Kludex/python-multipart#229).
- Cleanup metadata dunders from `__init__.py` [#&#8203;227](Kludex/python-multipart#227).
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box
---
This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTYiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNDYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->
See merge request swiss-armed-forces/cyber-command/cea/loom!460
Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Kludex
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Replace per-byte partial-boundary scan with rfind lookbehind by Kludex · Pull Request #300 · Kludex/python-multipart · GitHub
Skip to content

Replace per-byte partial-boundary scan with rfind lookbehind - #300

Merged
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data
Jun 4, 2026
Merged

Replace per-byte partial-boundary scan with rfind lookbehind#300
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data

Conversation

@Kludex

Copy link
Copy Markdown
Owner

What

When find cannot locate a whole boundary in part data, the tail may hold a partial boundary that completes in the next chunk. The old fallback seeded the per-byte matcher at the tail, which thrashed on CR/LF-dense bodies: \r\n matches the boundary prefix, fails at -, resets, reconsiders, and re-runs find for every \r\n pair near the chunk end.

Since self.boundary is \r\n-- + boundary and an RFC boundary cannot contain CR, the last CR in the tail is the only candidate prefix start. We anchor on it with rfind, confirm with startswith, carry the partial via index, and let the existing end-of-chunk flush emit the data and re-mark the lookbehind. This mirrors multer's lookbehind strategy.

Impact

Benchmarked against multipart_bench (1MB part body, 64KB chunks, sansio variant):

Scenariobeforeafter
worstcase_crlf~2700 MB/s~18500 MB/s (6.9x)
worstcase_lf~21500 MB/sunchanged
worstcase_bchar~16400 MB/sunchanged
file uploadunchangedunchanged

worstcase_crlf was the slowest scenario across all pure-Python parsers; it is now the fastest, reaching parity with the LF case.

Correctness

Verified behaviorally identical to the previous parser across 82k differential comparisons of the full callback-event stream and error type/offset: every chunk-split strategy (whole, byte-by-byte, fixed-size, random) plus an exhaustive two-chunk sweep with the boundary edge landing on every byte offset. Zero mismatches.

Adds a CRLF-dense corpus case (crlf_dense_part_data) to the data-driven test suite, run both whole-write and byte-by-byte. 100% coverage maintained.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

When `find` cannot locate a whole boundary in part data, the tail may
hold a partial boundary that completes in the next chunk. The old
fallback seeded the per-byte matcher at the tail, which thrashed on
CR/LF-dense bodies: `\r\n` matches the boundary prefix, fails at `-`,
resets, reconsiders, and re-runs `find` for every `\r\n` pair near the
chunk end.
Since `self.boundary` is `\r\n--` + boundary and an RFC boundary cannot
contain CR, the last CR in the tail is the only candidate prefix start.
Anchor on it with `rfind`, confirm with `startswith`, carry the partial
via `index`, and let the existing end-of-chunk flush emit the data and
re-mark the lookbehind. This mirrors multer's lookbehind strategy.
worstcase_crlf goes from ~2700 to ~18500 MB/s (6.9x), reaching parity
with the LF case; LF, boundary-char, and file-upload paths are
unchanged. Verified behaviorally identical to the previous parser across
82k differential comparisons (every chunk-split strategy incl.
byte-by-byte, with the boundary edge landing on every offset).
@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing speed-up-crlf-dense-part-data (12cbd2b) with main (4cffc68)

Open in CodSpeed

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@Kludex

Copy link
Copy Markdown
OwnerAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kludex
Kludex merged commit 8672979 into mainJun 4, 2026
15 checks passed
@Kludex
Kludex deleted the speed-up-crlf-dense-part-data branch June 4, 2026 15:42
@KludexKludex mentioned this pull request Jun 4, 2026
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Jun 29, 2026
This MR contains the following updates:
| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [debugpy](https://aka.ms/debugpy) ([source](https://github.com/microsoft/debugpy)) | dev | patch | `1.8.20` → `1.8.21` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microsoft/debugpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microsoft/debugpy) |
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.4.4` → `2.4.6` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) ([changelog](https://github.com/pydantic/pydantic-settings/releases)) | dependencies | patch | `2.14.0` → `2.14.2` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-settings/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-settings) |
| [python-multipart](https://github.com/Kludex/python-multipart) ([changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)) | dependencies | patch | `^0.0.22` → `^0.0.32` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/python-multipart/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/python-multipart) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.32.0.20240523` → `2.32.4.20260324` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
---
### Release Notes
<details>
<summary>microsoft/debugpy (debugpy)</summary>
### [`v1.8.21`](https://github.com/microsoft/debugpy/releases/tag/v1.8.21): debugpy v1.8.21
[Compare Source](microsoft/debugpy@v1.8.20...v1.8.21)
Fixes for:
- Return evaluate result in DAP response body instead of writing to stdout: [#&#8203;2027](microsoft/debugpy#2027)
- Prevent invalid `scopes` request from crashing debug session: [#&#8203;2026](microsoft/debugpy#2026)
- Skip uninitialized `__slots__` in variable resolver: [#&#8203;2024](microsoft/debugpy#2024)
- Handle `-c` arguments that are `bytes` instead of `str`: [#&#8203;2021](microsoft/debugpy#2021)
- Fix evaluation of variables from chained exception frames: [#&#8203;2018](microsoft/debugpy#2018)
- `ContinueRequest` with a specific `threadId` no longer resumes all threads (in-process adapter): [#&#8203;2012](microsoft/debugpy#2012)
- Avoid strong reference to exceptions during unwind: [#&#8203;2008](microsoft/debugpy#2008)
- Show error message on evaluate failures in the hover context: [#&#8203;2006](microsoft/debugpy#2006)
- Display `dlerror` output when `dlopen` fails: [#&#8203;2000](microsoft/debugpy#2000)
- Replace removed `pkgutil.get_loader` with `importlib.util.find_spec` in `get_fullname`: [#&#8203;1998](microsoft/debugpy#1998)
Enhancements:
- Add option to ignore all system exit codes: [#&#8203;2017](microsoft/debugpy#2017)
- Pull changes from pydevd up to March 2026: [#&#8203;2010](microsoft/debugpy#2010)
Infrastructure work:
- Suppress Flawfinder false positives on Cython memcpy / read-loop iterators (TSA [#&#8203;2816216](https://github.com/microsoft/debugpy/issues/2816216), [#&#8203;2816217](https://github.com/microsoft/debugpy/issues/2816217), [#&#8203;2816218](https://github.com/microsoft/debugpy/issues/2816218), [#&#8203;2816219](https://github.com/microsoft/debugpy/issues/2816219), [#&#8203;2816220](https://github.com/microsoft/debugpy/issues/2816220)): [#&#8203;2028](microsoft/debugpy#2028), [#&#8203;2029](microsoft/debugpy#2029), [#&#8203;2030](microsoft/debugpy#2030), [#&#8203;2031](microsoft/debugpy#2031), [#&#8203;2032](microsoft/debugpy#2032)
Thanks to [@&#8203;maxbachmann](https://github.com/maxbachmann), [@&#8203;mfussenegger](https://github.com/mfussenegger), and [@&#8203;sambrightman](https://github.com/sambrightman) for the commits.
</details>
<details>
<summary>numpy/numpy (numpy)</summary>
### [`v2.4.6`](https://github.com/numpy/numpy/releases/tag/v2.4.6): (May 18, 2026)
[Compare Source](numpy/numpy@v2.4.5...v2.4.6)
### NumPy 2.4.6 Release Notes
NumPy 2.4.6 is a quick release that fixes a regression discovered in the 2.4.5
release.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 4 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- !EarlMilktea
- Charles Harris
- Sebastian Berg
- Warren Weckesser
#### Pull requests merged
A total of 4 pull requests were merged for this release.
- [#&#8203;31444](numpy/numpy#31444): MAINT: Prepare 2.4.x for further development
- [#&#8203;31453](numpy/numpy#31453): BUG: Fix regression in `arr.conj()`
- [#&#8203;31459](numpy/numpy#31459): BUG: `np.linalg.svd(..., hermitian=True)` returns non-unitary...
- [#&#8203;31460](numpy/numpy#31460): BUG: Don't call INCREF/DECREF on descr in NpyStringAcquireAllocator...
### [`v2.4.5`](https://github.com/numpy/numpy/releases/tag/v2.4.5): (May 15, 2026)
[Compare Source](numpy/numpy@v2.4.4...v2.4.5)
### NumPy 2.4.5 Release Notes
NumPy 2.4.5 is a patch release that fixes bugs discovered after the 2.4.4
release, has some typing improvements, and maintains infrastructure.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 17 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- Aleksei Nikiforov
- Anarion Zuo +
- Ankit Ahlawat
- Breno Favaretto +
- Charles Harris
- Igor Krivenko +
- Ijtihed Kilani +
- Joren Hammudoglu
- Maarten Baert +
- Matti Picus
- Nathan Goldbaum
- Praneeth Kodumagulla +
- Ralf Gommers
- RoomWithOutRoof +
- Sebastian Berg
- Warren Weckesser
- div +
#### Pull requests merged
A total of 28 pull requests were merged for this release.
- [#&#8203;31093](numpy/numpy#31093): MAINT: Prepare 2.4.x for further development
- [#&#8203;31182](numpy/numpy#31182): TYP: fix `np.shape` assignability issue for python lists ([#&#8203;31171](numpy/numpy#31171))
- [#&#8203;31197](numpy/numpy#31197): ENH: Return rank 0 for empty matrices in matrix\_rank ([#&#8203;30422](numpy/numpy#30422))
- [#&#8203;31198](numpy/numpy#31198): CI/BUG: add native jobs for s390x, fix bug in `pack_inner`...
- [#&#8203;31199](numpy/numpy#31199): BUG: f2py map complex\_long\_double to NPY\_CLONGDOUBLE
- [#&#8203;31205](numpy/numpy#31205): MAINT: f2py: Stop setting re.\_MAXCACHE to 50.
- [#&#8203;31206](numpy/numpy#31206): BUG: fix heap buffer overflow in timedelta to string casts
- [#&#8203;31207](numpy/numpy#31207): MAINT: Rename ppc64le and s390x workflow ([#&#8203;31121](numpy/numpy#31121))
- [#&#8203;31208](numpy/numpy#31208): BUG: Fix matvec/vecmat in-place aliasing (out=input produces...
- [#&#8203;31209](numpy/numpy#31209): TYP: `tile`: accept numpy scalars and arrays as second argument...
- [#&#8203;31211](numpy/numpy#31211): DEP: Undo deprecation for np.dtype() signature used by old pickles...
- [#&#8203;31212](numpy/numpy#31212): REV: Manual revert of float16 svml use ([#&#8203;31178](numpy/numpy#31178))
- [#&#8203;31222](numpy/numpy#31222): TYP: `ix_` fix for boolean and non-1d input ([#&#8203;31218](numpy/numpy#31218))
- [#&#8203;31329](numpy/numpy#31329): BUG: incorrect temp elision for new-style (NEP 43) user-defined...
- [#&#8203;31330](numpy/numpy#31330): TYP: fix sliding\_window\_view axis parameter typing
- [#&#8203;31335](numpy/numpy#31335): BUG: Prevent deadlock due to downstream importing NumPy in dlopen...
- [#&#8203;31336](numpy/numpy#31336): BUG: Fix segfault in nditer.multi\_index when \_\_getitem\_\_ raises...
- [#&#8203;31338](numpy/numpy#31338): TYP: Fix ruff lint error
- [#&#8203;31357](numpy/numpy#31357): BUG: fix memory leak in np.zeros when fill-zero loop raises ([#&#8203;31320](numpy/numpy#31320))
- [#&#8203;31358](numpy/numpy#31358): BUG: np.einsum() fails with a 0-dimensional out argument and...
- [#&#8203;31379](numpy/numpy#31379): BUG: Fix signed overflow issue in npy\_gcd for INT\_MIN on s390x...
- [#&#8203;31383](numpy/numpy#31383): CI: remove Cirrus CI FreeBSD job ([#&#8203;31380](numpy/numpy#31380))
- [#&#8203;31390](numpy/numpy#31390): BUILD: newer MKL uses so.3
- [#&#8203;31391](numpy/numpy#31391): BLD/MAINT: improve support for Intel LLVM compilers
- [#&#8203;31401](numpy/numpy#31401): BUG: Avoid UB in [safe]()\[add,sub,mul] helpers ([#&#8203;31396](numpy/numpy#31396))
- [#&#8203;31402](numpy/numpy#31402): BUG: exclude \_\_pycache\_\_ directories from wheels ([#&#8203;31397](numpy/numpy#31397))
- [#&#8203;31404](numpy/numpy#31404): TYP: `_NestedSequence` type parameter default to work around...
- [#&#8203;31426](numpy/numpy#31426): TYP: Fix `DTypeLike` runtime type-checker support ([#&#8203;31425](numpy/numpy#31425))
</details>
<details>
<summary>pydantic/pydantic-settings (pydantic-settings)</summary>
### [`v2.14.2`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.2)
[Compare Source](pydantic/pydantic-settings@v2.14.1...v2.14.2)
#### What's Changed
This is a security patch release.
- Prevent `NestedSecretsSettingsSource` from following symlinks outside `secrets_dir` by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;889](pydantic/pydantic-settings#889)
- Prepare release 2.14.2 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;890](pydantic/pydantic-settings#890)
##### Security
Fixes [GHSA-4xgf-cpjx-pc3j](GHSA-4xgf-cpjx-pc3j): `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` could follow a symbolic link inside `secrets_dir` pointing outside it, reading out-of-tree files into settings values and bypassing the `secrets_dir_max_size` cap. Affected versions: `>= 2.12.0, < 2.14.2`.
**Full Changelog**: <pydantic/pydantic-settings@v2.14.1...v2.14.2>
### [`v2.14.1`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.1)
[Compare Source](pydantic/pydantic-settings@v2.14.0...v2.14.1)
#### What's Changed
- Bump the python-packages group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;850](pydantic/pydantic-settings#850)
- Bump the python-packages group with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;854](pydantic/pydantic-settings#854)
- Bump the github-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;853](pydantic/pydantic-settings#853)
- Bump the python-packages group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;856](pydantic/pydantic-settings#856)
- Fix field named `cls` conflicting with classmethod parameter by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;858](pydantic/pydantic-settings#858)
- Prepare release 2.14.1 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;859](pydantic/pydantic-settings#859)
**Full Changelog**: <pydantic/pydantic-settings@v2.14.0...v2.14.1>
</details>
<details>
<summary>Kludex/python-multipart (python-multipart)</summary>
### [`v0.0.32`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0032-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.31...0.0.32)
- Speed up partial-boundary scanning for CR/LF-dense part data [#&#8203;300](Kludex/python-multipart#300).
### [`v0.0.31`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0031-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.30...0.0.31)
- Speed up multipart header parsing and callback dispatch [#&#8203;295](Kludex/python-multipart#295).
- Bound header field name size before validating [#&#8203;296](Kludex/python-multipart#296).
- Validate `Content-Length` is non-negative in `parse_form` [#&#8203;297](Kludex/python-multipart#297).
### [`v0.0.30`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0030-2026-05-31)
[Compare Source](Kludex/python-multipart@0.0.29...0.0.30)
- Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator [#&#8203;290](Kludex/python-multipart#290).
- Ignore RFC 2231/5987 extended parameters (`name*`, `filename*`) in `parse_options_header`, keeping the plain parameter authoritative per [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) [#&#8203;291](Kludex/python-multipart#291).
### [`v0.0.29`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0029-2026-05-17)
[Compare Source](Kludex/python-multipart@0.0.28...0.0.29)
- Handle malformed RFC 2231 continuations in `parse_options_header` [#&#8203;270](Kludex/python-multipart#270).
### [`v0.0.28`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0028-2026-05-10)
[Compare Source](Kludex/python-multipart@0.0.27...0.0.28)
- Speed up partial-boundary tail scan via `bytes.find` [#&#8203;281](Kludex/python-multipart#281).
- Cap multipart boundary length at 256 bytes [#&#8203;282](Kludex/python-multipart#282).
### [`v0.0.27`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0027-2026-04-27)
[Compare Source](Kludex/python-multipart@0.0.26...0.0.27)
- Add multipart header limits [#&#8203;267](Kludex/python-multipart#267).
- Pass parse offsets via constructors [#&#8203;268](Kludex/python-multipart#268).
### [`v0.0.26`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0026-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.25...0.0.26)
- Skip preamble before the first multipart boundary more efficiently [#&#8203;262](Kludex/python-multipart#262).
- Silently discard epilogue data after the closing multipart boundary [#&#8203;259](Kludex/python-multipart#259).
### [`v0.0.25`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0025-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.24...0.0.25)
- Add MIME content type info to `File` [#&#8203;143](Kludex/python-multipart#143).
- Handle CTE values case-insensitively [#&#8203;258](Kludex/python-multipart#258).
- Remove custom `FormParser` classes [#&#8203;257](Kludex/python-multipart#257).
- Add `UPLOAD_DELETE_TMP` to `FormParser` config [#&#8203;254](Kludex/python-multipart#254).
- Emit `field_end` for trailing bare field names on finalize [#&#8203;230](Kludex/python-multipart#230).
- Handle multipart headers case-insensitively [#&#8203;252](Kludex/python-multipart#252).
- Apply Apache-2.0 properly [#&#8203;247](Kludex/python-multipart#247).
### [`v0.0.24`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0024-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.23...0.0.24)
- Validate `chunk_size` in `parse_form()` [#&#8203;244](Kludex/python-multipart#244).
### [`v0.0.23`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0023-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.22...0.0.23)
- Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#&#8203;196](Kludex/python-multipart#196).
- Return processed length from `QuerystringParser._internal_write` [#&#8203;229](Kludex/python-multipart#229).
- Cleanup metadata dunders from `__init__.py` [#&#8203;227](Kludex/python-multipart#227).
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box
---
This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTYiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNDYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->
See merge request swiss-armed-forces/cyber-command/cea/loom!460
Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Kludex
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Replace per-byte partial-boundary scan with rfind lookbehind by Kludex · Pull Request #300 · Kludex/python-multipart · GitHub
Skip to content

Replace per-byte partial-boundary scan with rfind lookbehind - #300

Merged
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data
Jun 4, 2026
Merged

Replace per-byte partial-boundary scan with rfind lookbehind#300
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data

Conversation

@Kludex

Copy link
Copy Markdown
Owner

What

When find cannot locate a whole boundary in part data, the tail may hold a partial boundary that completes in the next chunk. The old fallback seeded the per-byte matcher at the tail, which thrashed on CR/LF-dense bodies: \r\n matches the boundary prefix, fails at -, resets, reconsiders, and re-runs find for every \r\n pair near the chunk end.

Since self.boundary is \r\n-- + boundary and an RFC boundary cannot contain CR, the last CR in the tail is the only candidate prefix start. We anchor on it with rfind, confirm with startswith, carry the partial via index, and let the existing end-of-chunk flush emit the data and re-mark the lookbehind. This mirrors multer's lookbehind strategy.

Impact

Benchmarked against multipart_bench (1MB part body, 64KB chunks, sansio variant):

Scenariobeforeafter
worstcase_crlf~2700 MB/s~18500 MB/s (6.9x)
worstcase_lf~21500 MB/sunchanged
worstcase_bchar~16400 MB/sunchanged
file uploadunchangedunchanged

worstcase_crlf was the slowest scenario across all pure-Python parsers; it is now the fastest, reaching parity with the LF case.

Correctness

Verified behaviorally identical to the previous parser across 82k differential comparisons of the full callback-event stream and error type/offset: every chunk-split strategy (whole, byte-by-byte, fixed-size, random) plus an exhaustive two-chunk sweep with the boundary edge landing on every byte offset. Zero mismatches.

Adds a CRLF-dense corpus case (crlf_dense_part_data) to the data-driven test suite, run both whole-write and byte-by-byte. 100% coverage maintained.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

When `find` cannot locate a whole boundary in part data, the tail may
hold a partial boundary that completes in the next chunk. The old
fallback seeded the per-byte matcher at the tail, which thrashed on
CR/LF-dense bodies: `\r\n` matches the boundary prefix, fails at `-`,
resets, reconsiders, and re-runs `find` for every `\r\n` pair near the
chunk end.
Since `self.boundary` is `\r\n--` + boundary and an RFC boundary cannot
contain CR, the last CR in the tail is the only candidate prefix start.
Anchor on it with `rfind`, confirm with `startswith`, carry the partial
via `index`, and let the existing end-of-chunk flush emit the data and
re-mark the lookbehind. This mirrors multer's lookbehind strategy.
worstcase_crlf goes from ~2700 to ~18500 MB/s (6.9x), reaching parity
with the LF case; LF, boundary-char, and file-upload paths are
unchanged. Verified behaviorally identical to the previous parser across
82k differential comparisons (every chunk-split strategy incl.
byte-by-byte, with the boundary edge landing on every offset).
@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing speed-up-crlf-dense-part-data (12cbd2b) with main (4cffc68)

Open in CodSpeed

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@Kludex

Copy link
Copy Markdown
OwnerAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kludex
Kludex merged commit 8672979 into mainJun 4, 2026
15 checks passed
@Kludex
Kludex deleted the speed-up-crlf-dense-part-data branch June 4, 2026 15:42
@KludexKludex mentioned this pull request Jun 4, 2026
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Jun 29, 2026
This MR contains the following updates:
| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [debugpy](https://aka.ms/debugpy) ([source](https://github.com/microsoft/debugpy)) | dev | patch | `1.8.20` → `1.8.21` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microsoft/debugpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microsoft/debugpy) |
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.4.4` → `2.4.6` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) ([changelog](https://github.com/pydantic/pydantic-settings/releases)) | dependencies | patch | `2.14.0` → `2.14.2` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-settings/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-settings) |
| [python-multipart](https://github.com/Kludex/python-multipart) ([changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)) | dependencies | patch | `^0.0.22` → `^0.0.32` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/python-multipart/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/python-multipart) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.32.0.20240523` → `2.32.4.20260324` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
---
### Release Notes
<details>
<summary>microsoft/debugpy (debugpy)</summary>
### [`v1.8.21`](https://github.com/microsoft/debugpy/releases/tag/v1.8.21): debugpy v1.8.21
[Compare Source](microsoft/debugpy@v1.8.20...v1.8.21)
Fixes for:
- Return evaluate result in DAP response body instead of writing to stdout: [#&#8203;2027](microsoft/debugpy#2027)
- Prevent invalid `scopes` request from crashing debug session: [#&#8203;2026](microsoft/debugpy#2026)
- Skip uninitialized `__slots__` in variable resolver: [#&#8203;2024](microsoft/debugpy#2024)
- Handle `-c` arguments that are `bytes` instead of `str`: [#&#8203;2021](microsoft/debugpy#2021)
- Fix evaluation of variables from chained exception frames: [#&#8203;2018](microsoft/debugpy#2018)
- `ContinueRequest` with a specific `threadId` no longer resumes all threads (in-process adapter): [#&#8203;2012](microsoft/debugpy#2012)
- Avoid strong reference to exceptions during unwind: [#&#8203;2008](microsoft/debugpy#2008)
- Show error message on evaluate failures in the hover context: [#&#8203;2006](microsoft/debugpy#2006)
- Display `dlerror` output when `dlopen` fails: [#&#8203;2000](microsoft/debugpy#2000)
- Replace removed `pkgutil.get_loader` with `importlib.util.find_spec` in `get_fullname`: [#&#8203;1998](microsoft/debugpy#1998)
Enhancements:
- Add option to ignore all system exit codes: [#&#8203;2017](microsoft/debugpy#2017)
- Pull changes from pydevd up to March 2026: [#&#8203;2010](microsoft/debugpy#2010)
Infrastructure work:
- Suppress Flawfinder false positives on Cython memcpy / read-loop iterators (TSA [#&#8203;2816216](https://github.com/microsoft/debugpy/issues/2816216), [#&#8203;2816217](https://github.com/microsoft/debugpy/issues/2816217), [#&#8203;2816218](https://github.com/microsoft/debugpy/issues/2816218), [#&#8203;2816219](https://github.com/microsoft/debugpy/issues/2816219), [#&#8203;2816220](https://github.com/microsoft/debugpy/issues/2816220)): [#&#8203;2028](microsoft/debugpy#2028), [#&#8203;2029](microsoft/debugpy#2029), [#&#8203;2030](microsoft/debugpy#2030), [#&#8203;2031](microsoft/debugpy#2031), [#&#8203;2032](microsoft/debugpy#2032)
Thanks to [@&#8203;maxbachmann](https://github.com/maxbachmann), [@&#8203;mfussenegger](https://github.com/mfussenegger), and [@&#8203;sambrightman](https://github.com/sambrightman) for the commits.
</details>
<details>
<summary>numpy/numpy (numpy)</summary>
### [`v2.4.6`](https://github.com/numpy/numpy/releases/tag/v2.4.6): (May 18, 2026)
[Compare Source](numpy/numpy@v2.4.5...v2.4.6)
### NumPy 2.4.6 Release Notes
NumPy 2.4.6 is a quick release that fixes a regression discovered in the 2.4.5
release.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 4 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- !EarlMilktea
- Charles Harris
- Sebastian Berg
- Warren Weckesser
#### Pull requests merged
A total of 4 pull requests were merged for this release.
- [#&#8203;31444](numpy/numpy#31444): MAINT: Prepare 2.4.x for further development
- [#&#8203;31453](numpy/numpy#31453): BUG: Fix regression in `arr.conj()`
- [#&#8203;31459](numpy/numpy#31459): BUG: `np.linalg.svd(..., hermitian=True)` returns non-unitary...
- [#&#8203;31460](numpy/numpy#31460): BUG: Don't call INCREF/DECREF on descr in NpyStringAcquireAllocator...
### [`v2.4.5`](https://github.com/numpy/numpy/releases/tag/v2.4.5): (May 15, 2026)
[Compare Source](numpy/numpy@v2.4.4...v2.4.5)
### NumPy 2.4.5 Release Notes
NumPy 2.4.5 is a patch release that fixes bugs discovered after the 2.4.4
release, has some typing improvements, and maintains infrastructure.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 17 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- Aleksei Nikiforov
- Anarion Zuo +
- Ankit Ahlawat
- Breno Favaretto +
- Charles Harris
- Igor Krivenko +
- Ijtihed Kilani +
- Joren Hammudoglu
- Maarten Baert +
- Matti Picus
- Nathan Goldbaum
- Praneeth Kodumagulla +
- Ralf Gommers
- RoomWithOutRoof +
- Sebastian Berg
- Warren Weckesser
- div +
#### Pull requests merged
A total of 28 pull requests were merged for this release.
- [#&#8203;31093](numpy/numpy#31093): MAINT: Prepare 2.4.x for further development
- [#&#8203;31182](numpy/numpy#31182): TYP: fix `np.shape` assignability issue for python lists ([#&#8203;31171](numpy/numpy#31171))
- [#&#8203;31197](numpy/numpy#31197): ENH: Return rank 0 for empty matrices in matrix\_rank ([#&#8203;30422](numpy/numpy#30422))
- [#&#8203;31198](numpy/numpy#31198): CI/BUG: add native jobs for s390x, fix bug in `pack_inner`...
- [#&#8203;31199](numpy/numpy#31199): BUG: f2py map complex\_long\_double to NPY\_CLONGDOUBLE
- [#&#8203;31205](numpy/numpy#31205): MAINT: f2py: Stop setting re.\_MAXCACHE to 50.
- [#&#8203;31206](numpy/numpy#31206): BUG: fix heap buffer overflow in timedelta to string casts
- [#&#8203;31207](numpy/numpy#31207): MAINT: Rename ppc64le and s390x workflow ([#&#8203;31121](numpy/numpy#31121))
- [#&#8203;31208](numpy/numpy#31208): BUG: Fix matvec/vecmat in-place aliasing (out=input produces...
- [#&#8203;31209](numpy/numpy#31209): TYP: `tile`: accept numpy scalars and arrays as second argument...
- [#&#8203;31211](numpy/numpy#31211): DEP: Undo deprecation for np.dtype() signature used by old pickles...
- [#&#8203;31212](numpy/numpy#31212): REV: Manual revert of float16 svml use ([#&#8203;31178](numpy/numpy#31178))
- [#&#8203;31222](numpy/numpy#31222): TYP: `ix_` fix for boolean and non-1d input ([#&#8203;31218](numpy/numpy#31218))
- [#&#8203;31329](numpy/numpy#31329): BUG: incorrect temp elision for new-style (NEP 43) user-defined...
- [#&#8203;31330](numpy/numpy#31330): TYP: fix sliding\_window\_view axis parameter typing
- [#&#8203;31335](numpy/numpy#31335): BUG: Prevent deadlock due to downstream importing NumPy in dlopen...
- [#&#8203;31336](numpy/numpy#31336): BUG: Fix segfault in nditer.multi\_index when \_\_getitem\_\_ raises...
- [#&#8203;31338](numpy/numpy#31338): TYP: Fix ruff lint error
- [#&#8203;31357](numpy/numpy#31357): BUG: fix memory leak in np.zeros when fill-zero loop raises ([#&#8203;31320](numpy/numpy#31320))
- [#&#8203;31358](numpy/numpy#31358): BUG: np.einsum() fails with a 0-dimensional out argument and...
- [#&#8203;31379](numpy/numpy#31379): BUG: Fix signed overflow issue in npy\_gcd for INT\_MIN on s390x...
- [#&#8203;31383](numpy/numpy#31383): CI: remove Cirrus CI FreeBSD job ([#&#8203;31380](numpy/numpy#31380))
- [#&#8203;31390](numpy/numpy#31390): BUILD: newer MKL uses so.3
- [#&#8203;31391](numpy/numpy#31391): BLD/MAINT: improve support for Intel LLVM compilers
- [#&#8203;31401](numpy/numpy#31401): BUG: Avoid UB in [safe]()\[add,sub,mul] helpers ([#&#8203;31396](numpy/numpy#31396))
- [#&#8203;31402](numpy/numpy#31402): BUG: exclude \_\_pycache\_\_ directories from wheels ([#&#8203;31397](numpy/numpy#31397))
- [#&#8203;31404](numpy/numpy#31404): TYP: `_NestedSequence` type parameter default to work around...
- [#&#8203;31426](numpy/numpy#31426): TYP: Fix `DTypeLike` runtime type-checker support ([#&#8203;31425](numpy/numpy#31425))
</details>
<details>
<summary>pydantic/pydantic-settings (pydantic-settings)</summary>
### [`v2.14.2`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.2)
[Compare Source](pydantic/pydantic-settings@v2.14.1...v2.14.2)
#### What's Changed
This is a security patch release.
- Prevent `NestedSecretsSettingsSource` from following symlinks outside `secrets_dir` by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;889](pydantic/pydantic-settings#889)
- Prepare release 2.14.2 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;890](pydantic/pydantic-settings#890)
##### Security
Fixes [GHSA-4xgf-cpjx-pc3j](GHSA-4xgf-cpjx-pc3j): `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` could follow a symbolic link inside `secrets_dir` pointing outside it, reading out-of-tree files into settings values and bypassing the `secrets_dir_max_size` cap. Affected versions: `>= 2.12.0, < 2.14.2`.
**Full Changelog**: <pydantic/pydantic-settings@v2.14.1...v2.14.2>
### [`v2.14.1`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.1)
[Compare Source](pydantic/pydantic-settings@v2.14.0...v2.14.1)
#### What's Changed
- Bump the python-packages group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;850](pydantic/pydantic-settings#850)
- Bump the python-packages group with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;854](pydantic/pydantic-settings#854)
- Bump the github-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;853](pydantic/pydantic-settings#853)
- Bump the python-packages group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;856](pydantic/pydantic-settings#856)
- Fix field named `cls` conflicting with classmethod parameter by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;858](pydantic/pydantic-settings#858)
- Prepare release 2.14.1 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;859](pydantic/pydantic-settings#859)
**Full Changelog**: <pydantic/pydantic-settings@v2.14.0...v2.14.1>
</details>
<details>
<summary>Kludex/python-multipart (python-multipart)</summary>
### [`v0.0.32`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0032-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.31...0.0.32)
- Speed up partial-boundary scanning for CR/LF-dense part data [#&#8203;300](Kludex/python-multipart#300).
### [`v0.0.31`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0031-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.30...0.0.31)
- Speed up multipart header parsing and callback dispatch [#&#8203;295](Kludex/python-multipart#295).
- Bound header field name size before validating [#&#8203;296](Kludex/python-multipart#296).
- Validate `Content-Length` is non-negative in `parse_form` [#&#8203;297](Kludex/python-multipart#297).
### [`v0.0.30`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0030-2026-05-31)
[Compare Source](Kludex/python-multipart@0.0.29...0.0.30)
- Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator [#&#8203;290](Kludex/python-multipart#290).
- Ignore RFC 2231/5987 extended parameters (`name*`, `filename*`) in `parse_options_header`, keeping the plain parameter authoritative per [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) [#&#8203;291](Kludex/python-multipart#291).
### [`v0.0.29`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0029-2026-05-17)
[Compare Source](Kludex/python-multipart@0.0.28...0.0.29)
- Handle malformed RFC 2231 continuations in `parse_options_header` [#&#8203;270](Kludex/python-multipart#270).
### [`v0.0.28`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0028-2026-05-10)
[Compare Source](Kludex/python-multipart@0.0.27...0.0.28)
- Speed up partial-boundary tail scan via `bytes.find` [#&#8203;281](Kludex/python-multipart#281).
- Cap multipart boundary length at 256 bytes [#&#8203;282](Kludex/python-multipart#282).
### [`v0.0.27`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0027-2026-04-27)
[Compare Source](Kludex/python-multipart@0.0.26...0.0.27)
- Add multipart header limits [#&#8203;267](Kludex/python-multipart#267).
- Pass parse offsets via constructors [#&#8203;268](Kludex/python-multipart#268).
### [`v0.0.26`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0026-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.25...0.0.26)
- Skip preamble before the first multipart boundary more efficiently [#&#8203;262](Kludex/python-multipart#262).
- Silently discard epilogue data after the closing multipart boundary [#&#8203;259](Kludex/python-multipart#259).
### [`v0.0.25`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0025-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.24...0.0.25)
- Add MIME content type info to `File` [#&#8203;143](Kludex/python-multipart#143).
- Handle CTE values case-insensitively [#&#8203;258](Kludex/python-multipart#258).
- Remove custom `FormParser` classes [#&#8203;257](Kludex/python-multipart#257).
- Add `UPLOAD_DELETE_TMP` to `FormParser` config [#&#8203;254](Kludex/python-multipart#254).
- Emit `field_end` for trailing bare field names on finalize [#&#8203;230](Kludex/python-multipart#230).
- Handle multipart headers case-insensitively [#&#8203;252](Kludex/python-multipart#252).
- Apply Apache-2.0 properly [#&#8203;247](Kludex/python-multipart#247).
### [`v0.0.24`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0024-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.23...0.0.24)
- Validate `chunk_size` in `parse_form()` [#&#8203;244](Kludex/python-multipart#244).
### [`v0.0.23`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0023-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.22...0.0.23)
- Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#&#8203;196](Kludex/python-multipart#196).
- Return processed length from `QuerystringParser._internal_write` [#&#8203;229](Kludex/python-multipart#229).
- Cleanup metadata dunders from `__init__.py` [#&#8203;227](Kludex/python-multipart#227).
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box
---
This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTYiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNDYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->
See merge request swiss-armed-forces/cyber-command/cea/loom!460
Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Kludex
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Replace per-byte partial-boundary scan with rfind lookbehind by Kludex · Pull Request #300 · Kludex/python-multipart · GitHub
Skip to content

Replace per-byte partial-boundary scan with rfind lookbehind - #300

Merged
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data
Jun 4, 2026
Merged

Replace per-byte partial-boundary scan with rfind lookbehind#300
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data

Conversation

@Kludex

Copy link
Copy Markdown
Owner

What

When find cannot locate a whole boundary in part data, the tail may hold a partial boundary that completes in the next chunk. The old fallback seeded the per-byte matcher at the tail, which thrashed on CR/LF-dense bodies: \r\n matches the boundary prefix, fails at -, resets, reconsiders, and re-runs find for every \r\n pair near the chunk end.

Since self.boundary is \r\n-- + boundary and an RFC boundary cannot contain CR, the last CR in the tail is the only candidate prefix start. We anchor on it with rfind, confirm with startswith, carry the partial via index, and let the existing end-of-chunk flush emit the data and re-mark the lookbehind. This mirrors multer's lookbehind strategy.

Impact

Benchmarked against multipart_bench (1MB part body, 64KB chunks, sansio variant):

Scenariobeforeafter
worstcase_crlf~2700 MB/s~18500 MB/s (6.9x)
worstcase_lf~21500 MB/sunchanged
worstcase_bchar~16400 MB/sunchanged
file uploadunchangedunchanged

worstcase_crlf was the slowest scenario across all pure-Python parsers; it is now the fastest, reaching parity with the LF case.

Correctness

Verified behaviorally identical to the previous parser across 82k differential comparisons of the full callback-event stream and error type/offset: every chunk-split strategy (whole, byte-by-byte, fixed-size, random) plus an exhaustive two-chunk sweep with the boundary edge landing on every byte offset. Zero mismatches.

Adds a CRLF-dense corpus case (crlf_dense_part_data) to the data-driven test suite, run both whole-write and byte-by-byte. 100% coverage maintained.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

When `find` cannot locate a whole boundary in part data, the tail may
hold a partial boundary that completes in the next chunk. The old
fallback seeded the per-byte matcher at the tail, which thrashed on
CR/LF-dense bodies: `\r\n` matches the boundary prefix, fails at `-`,
resets, reconsiders, and re-runs `find` for every `\r\n` pair near the
chunk end.
Since `self.boundary` is `\r\n--` + boundary and an RFC boundary cannot
contain CR, the last CR in the tail is the only candidate prefix start.
Anchor on it with `rfind`, confirm with `startswith`, carry the partial
via `index`, and let the existing end-of-chunk flush emit the data and
re-mark the lookbehind. This mirrors multer's lookbehind strategy.
worstcase_crlf goes from ~2700 to ~18500 MB/s (6.9x), reaching parity
with the LF case; LF, boundary-char, and file-upload paths are
unchanged. Verified behaviorally identical to the previous parser across
82k differential comparisons (every chunk-split strategy incl.
byte-by-byte, with the boundary edge landing on every offset).
@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing speed-up-crlf-dense-part-data (12cbd2b) with main (4cffc68)

Open in CodSpeed

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@Kludex

Copy link
Copy Markdown
OwnerAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kludex
Kludex merged commit 8672979 into mainJun 4, 2026
15 checks passed
@Kludex
Kludex deleted the speed-up-crlf-dense-part-data branch June 4, 2026 15:42
@KludexKludex mentioned this pull request Jun 4, 2026
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Jun 29, 2026
This MR contains the following updates:
| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [debugpy](https://aka.ms/debugpy) ([source](https://github.com/microsoft/debugpy)) | dev | patch | `1.8.20` → `1.8.21` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microsoft/debugpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microsoft/debugpy) |
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.4.4` → `2.4.6` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) ([changelog](https://github.com/pydantic/pydantic-settings/releases)) | dependencies | patch | `2.14.0` → `2.14.2` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-settings/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-settings) |
| [python-multipart](https://github.com/Kludex/python-multipart) ([changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)) | dependencies | patch | `^0.0.22` → `^0.0.32` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/python-multipart/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/python-multipart) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.32.0.20240523` → `2.32.4.20260324` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
---
### Release Notes
<details>
<summary>microsoft/debugpy (debugpy)</summary>
### [`v1.8.21`](https://github.com/microsoft/debugpy/releases/tag/v1.8.21): debugpy v1.8.21
[Compare Source](microsoft/debugpy@v1.8.20...v1.8.21)
Fixes for:
- Return evaluate result in DAP response body instead of writing to stdout: [#&#8203;2027](microsoft/debugpy#2027)
- Prevent invalid `scopes` request from crashing debug session: [#&#8203;2026](microsoft/debugpy#2026)
- Skip uninitialized `__slots__` in variable resolver: [#&#8203;2024](microsoft/debugpy#2024)
- Handle `-c` arguments that are `bytes` instead of `str`: [#&#8203;2021](microsoft/debugpy#2021)
- Fix evaluation of variables from chained exception frames: [#&#8203;2018](microsoft/debugpy#2018)
- `ContinueRequest` with a specific `threadId` no longer resumes all threads (in-process adapter): [#&#8203;2012](microsoft/debugpy#2012)
- Avoid strong reference to exceptions during unwind: [#&#8203;2008](microsoft/debugpy#2008)
- Show error message on evaluate failures in the hover context: [#&#8203;2006](microsoft/debugpy#2006)
- Display `dlerror` output when `dlopen` fails: [#&#8203;2000](microsoft/debugpy#2000)
- Replace removed `pkgutil.get_loader` with `importlib.util.find_spec` in `get_fullname`: [#&#8203;1998](microsoft/debugpy#1998)
Enhancements:
- Add option to ignore all system exit codes: [#&#8203;2017](microsoft/debugpy#2017)
- Pull changes from pydevd up to March 2026: [#&#8203;2010](microsoft/debugpy#2010)
Infrastructure work:
- Suppress Flawfinder false positives on Cython memcpy / read-loop iterators (TSA [#&#8203;2816216](https://github.com/microsoft/debugpy/issues/2816216), [#&#8203;2816217](https://github.com/microsoft/debugpy/issues/2816217), [#&#8203;2816218](https://github.com/microsoft/debugpy/issues/2816218), [#&#8203;2816219](https://github.com/microsoft/debugpy/issues/2816219), [#&#8203;2816220](https://github.com/microsoft/debugpy/issues/2816220)): [#&#8203;2028](microsoft/debugpy#2028), [#&#8203;2029](microsoft/debugpy#2029), [#&#8203;2030](microsoft/debugpy#2030), [#&#8203;2031](microsoft/debugpy#2031), [#&#8203;2032](microsoft/debugpy#2032)
Thanks to [@&#8203;maxbachmann](https://github.com/maxbachmann), [@&#8203;mfussenegger](https://github.com/mfussenegger), and [@&#8203;sambrightman](https://github.com/sambrightman) for the commits.
</details>
<details>
<summary>numpy/numpy (numpy)</summary>
### [`v2.4.6`](https://github.com/numpy/numpy/releases/tag/v2.4.6): (May 18, 2026)
[Compare Source](numpy/numpy@v2.4.5...v2.4.6)
### NumPy 2.4.6 Release Notes
NumPy 2.4.6 is a quick release that fixes a regression discovered in the 2.4.5
release.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 4 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- !EarlMilktea
- Charles Harris
- Sebastian Berg
- Warren Weckesser
#### Pull requests merged
A total of 4 pull requests were merged for this release.
- [#&#8203;31444](numpy/numpy#31444): MAINT: Prepare 2.4.x for further development
- [#&#8203;31453](numpy/numpy#31453): BUG: Fix regression in `arr.conj()`
- [#&#8203;31459](numpy/numpy#31459): BUG: `np.linalg.svd(..., hermitian=True)` returns non-unitary...
- [#&#8203;31460](numpy/numpy#31460): BUG: Don't call INCREF/DECREF on descr in NpyStringAcquireAllocator...
### [`v2.4.5`](https://github.com/numpy/numpy/releases/tag/v2.4.5): (May 15, 2026)
[Compare Source](numpy/numpy@v2.4.4...v2.4.5)
### NumPy 2.4.5 Release Notes
NumPy 2.4.5 is a patch release that fixes bugs discovered after the 2.4.4
release, has some typing improvements, and maintains infrastructure.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 17 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- Aleksei Nikiforov
- Anarion Zuo +
- Ankit Ahlawat
- Breno Favaretto +
- Charles Harris
- Igor Krivenko +
- Ijtihed Kilani +
- Joren Hammudoglu
- Maarten Baert +
- Matti Picus
- Nathan Goldbaum
- Praneeth Kodumagulla +
- Ralf Gommers
- RoomWithOutRoof +
- Sebastian Berg
- Warren Weckesser
- div +
#### Pull requests merged
A total of 28 pull requests were merged for this release.
- [#&#8203;31093](numpy/numpy#31093): MAINT: Prepare 2.4.x for further development
- [#&#8203;31182](numpy/numpy#31182): TYP: fix `np.shape` assignability issue for python lists ([#&#8203;31171](numpy/numpy#31171))
- [#&#8203;31197](numpy/numpy#31197): ENH: Return rank 0 for empty matrices in matrix\_rank ([#&#8203;30422](numpy/numpy#30422))
- [#&#8203;31198](numpy/numpy#31198): CI/BUG: add native jobs for s390x, fix bug in `pack_inner`...
- [#&#8203;31199](numpy/numpy#31199): BUG: f2py map complex\_long\_double to NPY\_CLONGDOUBLE
- [#&#8203;31205](numpy/numpy#31205): MAINT: f2py: Stop setting re.\_MAXCACHE to 50.
- [#&#8203;31206](numpy/numpy#31206): BUG: fix heap buffer overflow in timedelta to string casts
- [#&#8203;31207](numpy/numpy#31207): MAINT: Rename ppc64le and s390x workflow ([#&#8203;31121](numpy/numpy#31121))
- [#&#8203;31208](numpy/numpy#31208): BUG: Fix matvec/vecmat in-place aliasing (out=input produces...
- [#&#8203;31209](numpy/numpy#31209): TYP: `tile`: accept numpy scalars and arrays as second argument...
- [#&#8203;31211](numpy/numpy#31211): DEP: Undo deprecation for np.dtype() signature used by old pickles...
- [#&#8203;31212](numpy/numpy#31212): REV: Manual revert of float16 svml use ([#&#8203;31178](numpy/numpy#31178))
- [#&#8203;31222](numpy/numpy#31222): TYP: `ix_` fix for boolean and non-1d input ([#&#8203;31218](numpy/numpy#31218))
- [#&#8203;31329](numpy/numpy#31329): BUG: incorrect temp elision for new-style (NEP 43) user-defined...
- [#&#8203;31330](numpy/numpy#31330): TYP: fix sliding\_window\_view axis parameter typing
- [#&#8203;31335](numpy/numpy#31335): BUG: Prevent deadlock due to downstream importing NumPy in dlopen...
- [#&#8203;31336](numpy/numpy#31336): BUG: Fix segfault in nditer.multi\_index when \_\_getitem\_\_ raises...
- [#&#8203;31338](numpy/numpy#31338): TYP: Fix ruff lint error
- [#&#8203;31357](numpy/numpy#31357): BUG: fix memory leak in np.zeros when fill-zero loop raises ([#&#8203;31320](numpy/numpy#31320))
- [#&#8203;31358](numpy/numpy#31358): BUG: np.einsum() fails with a 0-dimensional out argument and...
- [#&#8203;31379](numpy/numpy#31379): BUG: Fix signed overflow issue in npy\_gcd for INT\_MIN on s390x...
- [#&#8203;31383](numpy/numpy#31383): CI: remove Cirrus CI FreeBSD job ([#&#8203;31380](numpy/numpy#31380))
- [#&#8203;31390](numpy/numpy#31390): BUILD: newer MKL uses so.3
- [#&#8203;31391](numpy/numpy#31391): BLD/MAINT: improve support for Intel LLVM compilers
- [#&#8203;31401](numpy/numpy#31401): BUG: Avoid UB in [safe]()\[add,sub,mul] helpers ([#&#8203;31396](numpy/numpy#31396))
- [#&#8203;31402](numpy/numpy#31402): BUG: exclude \_\_pycache\_\_ directories from wheels ([#&#8203;31397](numpy/numpy#31397))
- [#&#8203;31404](numpy/numpy#31404): TYP: `_NestedSequence` type parameter default to work around...
- [#&#8203;31426](numpy/numpy#31426): TYP: Fix `DTypeLike` runtime type-checker support ([#&#8203;31425](numpy/numpy#31425))
</details>
<details>
<summary>pydantic/pydantic-settings (pydantic-settings)</summary>
### [`v2.14.2`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.2)
[Compare Source](pydantic/pydantic-settings@v2.14.1...v2.14.2)
#### What's Changed
This is a security patch release.
- Prevent `NestedSecretsSettingsSource` from following symlinks outside `secrets_dir` by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;889](pydantic/pydantic-settings#889)
- Prepare release 2.14.2 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;890](pydantic/pydantic-settings#890)
##### Security
Fixes [GHSA-4xgf-cpjx-pc3j](GHSA-4xgf-cpjx-pc3j): `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` could follow a symbolic link inside `secrets_dir` pointing outside it, reading out-of-tree files into settings values and bypassing the `secrets_dir_max_size` cap. Affected versions: `>= 2.12.0, < 2.14.2`.
**Full Changelog**: <pydantic/pydantic-settings@v2.14.1...v2.14.2>
### [`v2.14.1`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.1)
[Compare Source](pydantic/pydantic-settings@v2.14.0...v2.14.1)
#### What's Changed
- Bump the python-packages group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;850](pydantic/pydantic-settings#850)
- Bump the python-packages group with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;854](pydantic/pydantic-settings#854)
- Bump the github-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;853](pydantic/pydantic-settings#853)
- Bump the python-packages group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;856](pydantic/pydantic-settings#856)
- Fix field named `cls` conflicting with classmethod parameter by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;858](pydantic/pydantic-settings#858)
- Prepare release 2.14.1 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;859](pydantic/pydantic-settings#859)
**Full Changelog**: <pydantic/pydantic-settings@v2.14.0...v2.14.1>
</details>
<details>
<summary>Kludex/python-multipart (python-multipart)</summary>
### [`v0.0.32`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0032-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.31...0.0.32)
- Speed up partial-boundary scanning for CR/LF-dense part data [#&#8203;300](Kludex/python-multipart#300).
### [`v0.0.31`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0031-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.30...0.0.31)
- Speed up multipart header parsing and callback dispatch [#&#8203;295](Kludex/python-multipart#295).
- Bound header field name size before validating [#&#8203;296](Kludex/python-multipart#296).
- Validate `Content-Length` is non-negative in `parse_form` [#&#8203;297](Kludex/python-multipart#297).
### [`v0.0.30`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0030-2026-05-31)
[Compare Source](Kludex/python-multipart@0.0.29...0.0.30)
- Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator [#&#8203;290](Kludex/python-multipart#290).
- Ignore RFC 2231/5987 extended parameters (`name*`, `filename*`) in `parse_options_header`, keeping the plain parameter authoritative per [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) [#&#8203;291](Kludex/python-multipart#291).
### [`v0.0.29`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0029-2026-05-17)
[Compare Source](Kludex/python-multipart@0.0.28...0.0.29)
- Handle malformed RFC 2231 continuations in `parse_options_header` [#&#8203;270](Kludex/python-multipart#270).
### [`v0.0.28`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0028-2026-05-10)
[Compare Source](Kludex/python-multipart@0.0.27...0.0.28)
- Speed up partial-boundary tail scan via `bytes.find` [#&#8203;281](Kludex/python-multipart#281).
- Cap multipart boundary length at 256 bytes [#&#8203;282](Kludex/python-multipart#282).
### [`v0.0.27`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0027-2026-04-27)
[Compare Source](Kludex/python-multipart@0.0.26...0.0.27)
- Add multipart header limits [#&#8203;267](Kludex/python-multipart#267).
- Pass parse offsets via constructors [#&#8203;268](Kludex/python-multipart#268).
### [`v0.0.26`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0026-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.25...0.0.26)
- Skip preamble before the first multipart boundary more efficiently [#&#8203;262](Kludex/python-multipart#262).
- Silently discard epilogue data after the closing multipart boundary [#&#8203;259](Kludex/python-multipart#259).
### [`v0.0.25`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0025-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.24...0.0.25)
- Add MIME content type info to `File` [#&#8203;143](Kludex/python-multipart#143).
- Handle CTE values case-insensitively [#&#8203;258](Kludex/python-multipart#258).
- Remove custom `FormParser` classes [#&#8203;257](Kludex/python-multipart#257).
- Add `UPLOAD_DELETE_TMP` to `FormParser` config [#&#8203;254](Kludex/python-multipart#254).
- Emit `field_end` for trailing bare field names on finalize [#&#8203;230](Kludex/python-multipart#230).
- Handle multipart headers case-insensitively [#&#8203;252](Kludex/python-multipart#252).
- Apply Apache-2.0 properly [#&#8203;247](Kludex/python-multipart#247).
### [`v0.0.24`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0024-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.23...0.0.24)
- Validate `chunk_size` in `parse_form()` [#&#8203;244](Kludex/python-multipart#244).
### [`v0.0.23`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0023-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.22...0.0.23)
- Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#&#8203;196](Kludex/python-multipart#196).
- Return processed length from `QuerystringParser._internal_write` [#&#8203;229](Kludex/python-multipart#229).
- Cleanup metadata dunders from `__init__.py` [#&#8203;227](Kludex/python-multipart#227).
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box
---
This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTYiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNDYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->
See merge request swiss-armed-forces/cyber-command/cea/loom!460
Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Kludex
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Replace per-byte partial-boundary scan with rfind lookbehind by Kludex · Pull Request #300 · Kludex/python-multipart · GitHub
Skip to content

Replace per-byte partial-boundary scan with rfind lookbehind - #300

Merged
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data
Jun 4, 2026
Merged

Replace per-byte partial-boundary scan with rfind lookbehind#300
Kludex merged 1 commit into
mainfrom
speed-up-crlf-dense-part-data

Conversation

@Kludex

Copy link
Copy Markdown
Owner

What

When find cannot locate a whole boundary in part data, the tail may hold a partial boundary that completes in the next chunk. The old fallback seeded the per-byte matcher at the tail, which thrashed on CR/LF-dense bodies: \r\n matches the boundary prefix, fails at -, resets, reconsiders, and re-runs find for every \r\n pair near the chunk end.

Since self.boundary is \r\n-- + boundary and an RFC boundary cannot contain CR, the last CR in the tail is the only candidate prefix start. We anchor on it with rfind, confirm with startswith, carry the partial via index, and let the existing end-of-chunk flush emit the data and re-mark the lookbehind. This mirrors multer's lookbehind strategy.

Impact

Benchmarked against multipart_bench (1MB part body, 64KB chunks, sansio variant):

Scenariobeforeafter
worstcase_crlf~2700 MB/s~18500 MB/s (6.9x)
worstcase_lf~21500 MB/sunchanged
worstcase_bchar~16400 MB/sunchanged
file uploadunchangedunchanged

worstcase_crlf was the slowest scenario across all pure-Python parsers; it is now the fastest, reaching parity with the LF case.

Correctness

Verified behaviorally identical to the previous parser across 82k differential comparisons of the full callback-event stream and error type/offset: every chunk-split strategy (whole, byte-by-byte, fixed-size, random) plus an exhaustive two-chunk sweep with the boundary edge landing on every byte offset. Zero mismatches.

Adds a CRLF-dense corpus case (crlf_dense_part_data) to the data-driven test suite, run both whole-write and byte-by-byte. 100% coverage maintained.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

When `find` cannot locate a whole boundary in part data, the tail may
hold a partial boundary that completes in the next chunk. The old
fallback seeded the per-byte matcher at the tail, which thrashed on
CR/LF-dense bodies: `\r\n` matches the boundary prefix, fails at `-`,
resets, reconsiders, and re-runs `find` for every `\r\n` pair near the
chunk end.
Since `self.boundary` is `\r\n--` + boundary and an RFC boundary cannot
contain CR, the last CR in the tail is the only candidate prefix start.
Anchor on it with `rfind`, confirm with `startswith`, carry the partial
via `index`, and let the existing end-of-chunk flush emit the data and
re-mark the lookbehind. This mirrors multer's lookbehind strategy.
worstcase_crlf goes from ~2700 to ~18500 MB/s (6.9x), reaching parity
with the LF case; LF, boundary-char, and file-upload paths are
unchanged. Verified behaviorally identical to the previous parser across
82k differential comparisons (every chunk-split strategy incl.
byte-by-byte, with the boundary edge landing on every offset).
@codspeed-hq

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing speed-up-crlf-dense-part-data (12cbd2b) with main (4cffc68)

Open in CodSpeed

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

@Kludex

Copy link
Copy Markdown
OwnerAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Kludex
Kludex merged commit 8672979 into mainJun 4, 2026
15 checks passed
@Kludex
Kludex deleted the speed-up-crlf-dense-part-data branch June 4, 2026 15:42
@KludexKludex mentioned this pull request Jun 4, 2026
736-c41-2c1-e464fc974 added a commit to Swiss-Armed-Forces/Loom that referenced this pull request Jun 29, 2026
This MR contains the following updates:
| Package | Type | Update | Change | OpenSSF |
|---|---|---|---|---|
| [debugpy](https://aka.ms/debugpy) ([source](https://github.com/microsoft/debugpy)) | dev | patch | `1.8.20` → `1.8.21` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/microsoft/debugpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/microsoft/debugpy) |
| [numpy](https://github.com/numpy/numpy) ([changelog](https://numpy.org/doc/stable/release)) | dependencies | patch | `2.4.4` → `2.4.6` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/numpy/numpy/badge)](https://securityscorecards.dev/viewer/?uri=github.com/numpy/numpy) |
| [pydantic-settings](https://github.com/pydantic/pydantic-settings) ([changelog](https://github.com/pydantic/pydantic-settings/releases)) | dependencies | patch | `2.14.0` → `2.14.2` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/pydantic/pydantic-settings/badge)](https://securityscorecards.dev/viewer/?uri=github.com/pydantic/pydantic-settings) |
| [python-multipart](https://github.com/Kludex/python-multipart) ([changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md)) | dependencies | patch | `^0.0.22` → `^0.0.32` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Kludex/python-multipart/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Kludex/python-multipart) |
| [types-requests](https://github.com/python/typeshed) ([changelog](https://github.com/typeshed-internal/stub_uploader/blob/main/data/changelogs/requests.md)) | dependencies | patch | `2.32.0.20240523` → `2.32.4.20260324` | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/python/typeshed/badge)](https://securityscorecards.dev/viewer/?uri=github.com/python/typeshed) |
---
### Release Notes
<details>
<summary>microsoft/debugpy (debugpy)</summary>
### [`v1.8.21`](https://github.com/microsoft/debugpy/releases/tag/v1.8.21): debugpy v1.8.21
[Compare Source](microsoft/debugpy@v1.8.20...v1.8.21)
Fixes for:
- Return evaluate result in DAP response body instead of writing to stdout: [#&#8203;2027](microsoft/debugpy#2027)
- Prevent invalid `scopes` request from crashing debug session: [#&#8203;2026](microsoft/debugpy#2026)
- Skip uninitialized `__slots__` in variable resolver: [#&#8203;2024](microsoft/debugpy#2024)
- Handle `-c` arguments that are `bytes` instead of `str`: [#&#8203;2021](microsoft/debugpy#2021)
- Fix evaluation of variables from chained exception frames: [#&#8203;2018](microsoft/debugpy#2018)
- `ContinueRequest` with a specific `threadId` no longer resumes all threads (in-process adapter): [#&#8203;2012](microsoft/debugpy#2012)
- Avoid strong reference to exceptions during unwind: [#&#8203;2008](microsoft/debugpy#2008)
- Show error message on evaluate failures in the hover context: [#&#8203;2006](microsoft/debugpy#2006)
- Display `dlerror` output when `dlopen` fails: [#&#8203;2000](microsoft/debugpy#2000)
- Replace removed `pkgutil.get_loader` with `importlib.util.find_spec` in `get_fullname`: [#&#8203;1998](microsoft/debugpy#1998)
Enhancements:
- Add option to ignore all system exit codes: [#&#8203;2017](microsoft/debugpy#2017)
- Pull changes from pydevd up to March 2026: [#&#8203;2010](microsoft/debugpy#2010)
Infrastructure work:
- Suppress Flawfinder false positives on Cython memcpy / read-loop iterators (TSA [#&#8203;2816216](https://github.com/microsoft/debugpy/issues/2816216), [#&#8203;2816217](https://github.com/microsoft/debugpy/issues/2816217), [#&#8203;2816218](https://github.com/microsoft/debugpy/issues/2816218), [#&#8203;2816219](https://github.com/microsoft/debugpy/issues/2816219), [#&#8203;2816220](https://github.com/microsoft/debugpy/issues/2816220)): [#&#8203;2028](microsoft/debugpy#2028), [#&#8203;2029](microsoft/debugpy#2029), [#&#8203;2030](microsoft/debugpy#2030), [#&#8203;2031](microsoft/debugpy#2031), [#&#8203;2032](microsoft/debugpy#2032)
Thanks to [@&#8203;maxbachmann](https://github.com/maxbachmann), [@&#8203;mfussenegger](https://github.com/mfussenegger), and [@&#8203;sambrightman](https://github.com/sambrightman) for the commits.
</details>
<details>
<summary>numpy/numpy (numpy)</summary>
### [`v2.4.6`](https://github.com/numpy/numpy/releases/tag/v2.4.6): (May 18, 2026)
[Compare Source](numpy/numpy@v2.4.5...v2.4.6)
### NumPy 2.4.6 Release Notes
NumPy 2.4.6 is a quick release that fixes a regression discovered in the 2.4.5
release.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 4 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- !EarlMilktea
- Charles Harris
- Sebastian Berg
- Warren Weckesser
#### Pull requests merged
A total of 4 pull requests were merged for this release.
- [#&#8203;31444](numpy/numpy#31444): MAINT: Prepare 2.4.x for further development
- [#&#8203;31453](numpy/numpy#31453): BUG: Fix regression in `arr.conj()`
- [#&#8203;31459](numpy/numpy#31459): BUG: `np.linalg.svd(..., hermitian=True)` returns non-unitary...
- [#&#8203;31460](numpy/numpy#31460): BUG: Don't call INCREF/DECREF on descr in NpyStringAcquireAllocator...
### [`v2.4.5`](https://github.com/numpy/numpy/releases/tag/v2.4.5): (May 15, 2026)
[Compare Source](numpy/numpy@v2.4.4...v2.4.5)
### NumPy 2.4.5 Release Notes
NumPy 2.4.5 is a patch release that fixes bugs discovered after the 2.4.4
release, has some typing improvements, and maintains infrastructure.
This release supports Python versions 3.11-3.14
#### Contributors
A total of 17 people contributed to this release. People with a "+" by their
names contributed a patch for the first time.
- Aleksei Nikiforov
- Anarion Zuo +
- Ankit Ahlawat
- Breno Favaretto +
- Charles Harris
- Igor Krivenko +
- Ijtihed Kilani +
- Joren Hammudoglu
- Maarten Baert +
- Matti Picus
- Nathan Goldbaum
- Praneeth Kodumagulla +
- Ralf Gommers
- RoomWithOutRoof +
- Sebastian Berg
- Warren Weckesser
- div +
#### Pull requests merged
A total of 28 pull requests were merged for this release.
- [#&#8203;31093](numpy/numpy#31093): MAINT: Prepare 2.4.x for further development
- [#&#8203;31182](numpy/numpy#31182): TYP: fix `np.shape` assignability issue for python lists ([#&#8203;31171](numpy/numpy#31171))
- [#&#8203;31197](numpy/numpy#31197): ENH: Return rank 0 for empty matrices in matrix\_rank ([#&#8203;30422](numpy/numpy#30422))
- [#&#8203;31198](numpy/numpy#31198): CI/BUG: add native jobs for s390x, fix bug in `pack_inner`...
- [#&#8203;31199](numpy/numpy#31199): BUG: f2py map complex\_long\_double to NPY\_CLONGDOUBLE
- [#&#8203;31205](numpy/numpy#31205): MAINT: f2py: Stop setting re.\_MAXCACHE to 50.
- [#&#8203;31206](numpy/numpy#31206): BUG: fix heap buffer overflow in timedelta to string casts
- [#&#8203;31207](numpy/numpy#31207): MAINT: Rename ppc64le and s390x workflow ([#&#8203;31121](numpy/numpy#31121))
- [#&#8203;31208](numpy/numpy#31208): BUG: Fix matvec/vecmat in-place aliasing (out=input produces...
- [#&#8203;31209](numpy/numpy#31209): TYP: `tile`: accept numpy scalars and arrays as second argument...
- [#&#8203;31211](numpy/numpy#31211): DEP: Undo deprecation for np.dtype() signature used by old pickles...
- [#&#8203;31212](numpy/numpy#31212): REV: Manual revert of float16 svml use ([#&#8203;31178](numpy/numpy#31178))
- [#&#8203;31222](numpy/numpy#31222): TYP: `ix_` fix for boolean and non-1d input ([#&#8203;31218](numpy/numpy#31218))
- [#&#8203;31329](numpy/numpy#31329): BUG: incorrect temp elision for new-style (NEP 43) user-defined...
- [#&#8203;31330](numpy/numpy#31330): TYP: fix sliding\_window\_view axis parameter typing
- [#&#8203;31335](numpy/numpy#31335): BUG: Prevent deadlock due to downstream importing NumPy in dlopen...
- [#&#8203;31336](numpy/numpy#31336): BUG: Fix segfault in nditer.multi\_index when \_\_getitem\_\_ raises...
- [#&#8203;31338](numpy/numpy#31338): TYP: Fix ruff lint error
- [#&#8203;31357](numpy/numpy#31357): BUG: fix memory leak in np.zeros when fill-zero loop raises ([#&#8203;31320](numpy/numpy#31320))
- [#&#8203;31358](numpy/numpy#31358): BUG: np.einsum() fails with a 0-dimensional out argument and...
- [#&#8203;31379](numpy/numpy#31379): BUG: Fix signed overflow issue in npy\_gcd for INT\_MIN on s390x...
- [#&#8203;31383](numpy/numpy#31383): CI: remove Cirrus CI FreeBSD job ([#&#8203;31380](numpy/numpy#31380))
- [#&#8203;31390](numpy/numpy#31390): BUILD: newer MKL uses so.3
- [#&#8203;31391](numpy/numpy#31391): BLD/MAINT: improve support for Intel LLVM compilers
- [#&#8203;31401](numpy/numpy#31401): BUG: Avoid UB in [safe]()\[add,sub,mul] helpers ([#&#8203;31396](numpy/numpy#31396))
- [#&#8203;31402](numpy/numpy#31402): BUG: exclude \_\_pycache\_\_ directories from wheels ([#&#8203;31397](numpy/numpy#31397))
- [#&#8203;31404](numpy/numpy#31404): TYP: `_NestedSequence` type parameter default to work around...
- [#&#8203;31426](numpy/numpy#31426): TYP: Fix `DTypeLike` runtime type-checker support ([#&#8203;31425](numpy/numpy#31425))
</details>
<details>
<summary>pydantic/pydantic-settings (pydantic-settings)</summary>
### [`v2.14.2`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.2)
[Compare Source](pydantic/pydantic-settings@v2.14.1...v2.14.2)
#### What's Changed
This is a security patch release.
- Prevent `NestedSecretsSettingsSource` from following symlinks outside `secrets_dir` by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;889](pydantic/pydantic-settings#889)
- Prepare release 2.14.2 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;890](pydantic/pydantic-settings#890)
##### Security
Fixes [GHSA-4xgf-cpjx-pc3j](GHSA-4xgf-cpjx-pc3j): `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` could follow a symbolic link inside `secrets_dir` pointing outside it, reading out-of-tree files into settings values and bypassing the `secrets_dir_max_size` cap. Affected versions: `>= 2.12.0, < 2.14.2`.
**Full Changelog**: <pydantic/pydantic-settings@v2.14.1...v2.14.2>
### [`v2.14.1`](https://github.com/pydantic/pydantic-settings/releases/tag/v2.14.1)
[Compare Source](pydantic/pydantic-settings@v2.14.0...v2.14.1)
#### What's Changed
- Bump the python-packages group with 4 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;850](pydantic/pydantic-settings#850)
- Bump the python-packages group with 5 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;854](pydantic/pydantic-settings#854)
- Bump the github-actions group with 3 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;853](pydantic/pydantic-settings#853)
- Bump the python-packages group with 2 updates by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;856](pydantic/pydantic-settings#856)
- Fix field named `cls` conflicting with classmethod parameter by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;858](pydantic/pydantic-settings#858)
- Prepare release 2.14.1 by [@&#8203;hramezani](https://github.com/hramezani) in [#&#8203;859](pydantic/pydantic-settings#859)
**Full Changelog**: <pydantic/pydantic-settings@v2.14.0...v2.14.1>
</details>
<details>
<summary>Kludex/python-multipart (python-multipart)</summary>
### [`v0.0.32`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0032-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.31...0.0.32)
- Speed up partial-boundary scanning for CR/LF-dense part data [#&#8203;300](Kludex/python-multipart#300).
### [`v0.0.31`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0031-2026-06-04)
[Compare Source](Kludex/python-multipart@0.0.30...0.0.31)
- Speed up multipart header parsing and callback dispatch [#&#8203;295](Kludex/python-multipart#295).
- Bound header field name size before validating [#&#8203;296](Kludex/python-multipart#296).
- Validate `Content-Length` is non-negative in `parse_form` [#&#8203;297](Kludex/python-multipart#297).
### [`v0.0.30`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0030-2026-05-31)
[Compare Source](Kludex/python-multipart@0.0.29...0.0.30)
- Parse `application/x-www-form-urlencoded` bodies per the WHATWG URL standard, treating only `&` as a field separator [#&#8203;290](Kludex/python-multipart#290).
- Ignore RFC 2231/5987 extended parameters (`name*`, `filename*`) in `parse_options_header`, keeping the plain parameter authoritative per [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) [#&#8203;291](Kludex/python-multipart#291).
### [`v0.0.29`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0029-2026-05-17)
[Compare Source](Kludex/python-multipart@0.0.28...0.0.29)
- Handle malformed RFC 2231 continuations in `parse_options_header` [#&#8203;270](Kludex/python-multipart#270).
### [`v0.0.28`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0028-2026-05-10)
[Compare Source](Kludex/python-multipart@0.0.27...0.0.28)
- Speed up partial-boundary tail scan via `bytes.find` [#&#8203;281](Kludex/python-multipart#281).
- Cap multipart boundary length at 256 bytes [#&#8203;282](Kludex/python-multipart#282).
### [`v0.0.27`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0027-2026-04-27)
[Compare Source](Kludex/python-multipart@0.0.26...0.0.27)
- Add multipart header limits [#&#8203;267](Kludex/python-multipart#267).
- Pass parse offsets via constructors [#&#8203;268](Kludex/python-multipart#268).
### [`v0.0.26`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0026-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.25...0.0.26)
- Skip preamble before the first multipart boundary more efficiently [#&#8203;262](Kludex/python-multipart#262).
- Silently discard epilogue data after the closing multipart boundary [#&#8203;259](Kludex/python-multipart#259).
### [`v0.0.25`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0025-2026-04-10)
[Compare Source](Kludex/python-multipart@0.0.24...0.0.25)
- Add MIME content type info to `File` [#&#8203;143](Kludex/python-multipart#143).
- Handle CTE values case-insensitively [#&#8203;258](Kludex/python-multipart#258).
- Remove custom `FormParser` classes [#&#8203;257](Kludex/python-multipart#257).
- Add `UPLOAD_DELETE_TMP` to `FormParser` config [#&#8203;254](Kludex/python-multipart#254).
- Emit `field_end` for trailing bare field names on finalize [#&#8203;230](Kludex/python-multipart#230).
- Handle multipart headers case-insensitively [#&#8203;252](Kludex/python-multipart#252).
- Apply Apache-2.0 properly [#&#8203;247](Kludex/python-multipart#247).
### [`v0.0.24`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0024-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.23...0.0.24)
- Validate `chunk_size` in `parse_form()` [#&#8203;244](Kludex/python-multipart#244).
### [`v0.0.23`](https://github.com/Kludex/python-multipart/blob/HEAD/CHANGELOG.md#0023-2026-04-05)
[Compare Source](Kludex/python-multipart@0.0.22...0.0.23)
- Remove unused `trust_x_headers` parameter and `X-File-Name` fallback [#&#8203;196](Kludex/python-multipart#196).
- Return processed length from `QuerystringParser._internal_write` [#&#8203;229](Kludex/python-multipart#229).
- Cleanup metadata dunders from `__init__.py` [#&#8203;227](Kludex/python-multipart#227).
</details>
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box
---
This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMTAuMTYiLCJ1cGRhdGVkSW5WZXIiOiI0My4yNDYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIiwicmVub3ZhdGUiXX0=-->
See merge request swiss-armed-forces/cyber-command/cea/loom!460
Co-authored-by: Loom MR Pipeline Trigger <group_103951964_bot_9504bb8dead6d4e406ad817a607f24be@noreply.gitlab.com>
Co-authored-by: shrewd-laidback palace <shrewd-laidback-palace-736-c41-2c1-e464fc974@swiss-armed-forces-open-source.ch>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Kludex