Skip to content

fix(python): 3.14 의 응답 버퍼 선읽기가 바이트 예산을 본문 상한보다 먼저 터뜨렸다 — 예산이 걸린 원문 읽기를 8 KiB 로 묶는다 - #925

Merged
xzawed merged 1 commit into
mainfrom
fix/python-wire-readahead-314
Oct 31, 2026
Merged

xzawed merged 1 commit into
mainfrom
fix/python-wire-readahead-314

Conversation

@xzawed

@xzawed xzawed commented Oct 31, 2026

Copy link
Copy Markdown
Owner

무엇

Python 3.14 에서 상한(1,048,576 B)을 넘는 토큰·introspection·logout·admin 그랜트 응답이 가끔 KeycloakAuthError("… exceeds 1048576 bytes") 대신 KeycloakTransportError("Can't connect to server") 로 보고됐다. 바이트 예산(_internal/wire.py, py-v1.0.6 뒤 #893 에서 들어옴 — 미게시)이 본문 상한보다 먼저 터졌기 때문이다. 예산이 걸린 동안 원문 읽기 하나를 8 KiB 로 묶어, 판과 무관하게 본문 상한이 먼저 판정하게 한다. 문서화된 숫자(65,536 · 1,114,112 · 116,736)는 그대로다.

python 1.0.7 의 릴리스 요청은 이 PR 뒤에 낸다(일곱째 물결의 나머지 여덟은 진행 중).

원인 (측정)

수정

room() = min(남은 예산 + 1, _READ_AHEAD_BYTES = 8_192)(예산이 없거나 lift 뒤면 None — admin 성공 본문은 묶지 않는다). 버리친 대안: 틀 몫을 버퍼 크기만큼 키우기(문서화된 숫자·교차언어 동형이 바뀐다) · http.client 버퍼 교체(침습적).

시험 (RED → GREEN)

새 결정적 시험 둘 — test_wire.py::test_a_budgeted_read_asks_for_8_kib_however_large_the_buffer(131,072 B 버퍼를 늘 채우는 가짜 읽기) · test_token_response_cap.py::test_the_body_cap_wins_over_buffer_read_ahead(다섯 레인 × content-length/chunked, 버퍼 131,072 강제 · 소켓 읽기가 늘 가득 · 청크 경계를 상한 바로 아래에).

측정 수정 전 수정 후
구현자 — 로컬 3.13.11, 새 시험 20 회 20/20 회 같은 6 실패 20/20 회 11 통과
구현자 — Docker 3.10 · 3.11 · 3.12 · 3.13 · 3.14, 새 시험 각 3 회 판마다 6 failed, 5 passed 판마다 11 passed
구현자 — 원래 시험, 실제 3.14.10, 30 회 12/30 회 실패(청크 150 칸 중 16) 0/30
PM 재측정 — 로컬 3.13.11, 새 시험, 부모 wire.py 대 이 커밋 6 failed, 5 passed 11 passed

게이트(구현자): pytest 1545 passed · 커버리지 100.00% · ruff · ruff format · mypy strict — 로컬 3.13 과 3.14 컨테이너 모두. check-docs rc 0.

문서

wire.py 머리말에 ⚠️ 한 문단. .claude/rules/python.md Gotchas 에 한 줄(3.14 의 131,072 B 선읽기 · 시험 포인터) — doc-budget(4,690 B)은 올리지 않고 같은 파일의 되풀이 세 군데를 깎아 맞췄다(판정 방법 — mypy 2 → 0 측정 · client_secret 이 든 POST 본문 — 은 그대로).

[no-changelog: 미게시 결함 — 바이트 예산은 py-v1.0.6 에 없었고 1.0.7 은 아직 게시 전이라 소비자에게 보이는 변화가 없다]

🤖 Generated with Claude Code

https://claude.ai/code/session_01R4jYS4tpj7usahf8MK5dXo

… 로 묶는다

CI Python 3.14(3.14.9)에서 test_token_response_cap.py::test_a_huge_body_is_refused_holding_only_what_was_read
의 청크 레인이 간헐적으로 실패했다 — 16 MiB 토큰 응답을 `KeycloakAuthError("… exceeds 1048576 bytes")`
대신 `KeycloakTransportError("Can't connect to server")` 로 보고했다. run 41170195060 시도 1(cc-chunked)·
시도 2(cc-chunked·admin-chunked), run 41025781639 시도 1(introspect-chunked)·시도 2(admin-chunked).
같은 실행들의 3.10~3.13 은 초록.

원인 — `_internal/wire.py` 의 응답당 바이트 예산(본문 상한 + 틀 몫 65,536 = 1,114,112)은 http.client
응답 버퍼(`sock.makefile("rb")`, 크기 `io.DEFAULT_BUFFER_SIZE`) 아래의 원문 `readinto` 에서 센다 —
버퍼가 미리 받아 http.client 가 아직 쓰지 않은 바이트도 센다.
- Docker 프로브: python:3.14-slim(3.14.10) 131,072 · python:3.13-slim(3.13.18) 8,192. `readline`·
  `read(2)`(청크 끝 CRLF)가 원문에 청하는 크기도 같다. 빈 버퍼의 `read1(n)` 은 n 그대로라 길이
  레인은 머리 뒤로 미리 읽지 않는다(CI 실패도 청크 레인뿐).
- 그래서 3.14 에서는 상한 아래 64 KiB 안의 청크 경계에서 버퍼가 차면 본문이 1,048,577 바이트에
  닿기 전에 예산이 끊는다. 실 3.14.10 에서 사슬을 읽었다(경계가 떼기 전의 원인을 읽는 진단
  플러그인 · 수정 전 원래 시험 20 회 중 실패 7 건 전부): KeycloakConnectionError("Can't connect to
  server") ← urllib3 ProtocolError ← http.client.HTTPException("HTTP response exceeds 1114112 bytes
  on the wire"). `lower.refused_body` 가 원인에서 `ResponseRefused` 를 못 찾아 전송 오류가 된다.

수정 — 예산이 걸린 동안 원문 읽기 하나가 청하는 바이트를 8 KiB(`_READ_AHEAD_BYTES` = 3.13 까지의
`io.DEFAULT_BUFFER_SIZE`)로 묶는다. 「이번 읽기가 청해도 되는 바이트」의 정의인 `exchange` 의
`room()` 을 `min(남은 예산 + 1, 8,192)` 로 — `_metered.readinto` 가 이미 그 값으로 버퍼를 자른다.
버퍼의 선읽기는 원문 읽기 하나를 넘지 않으므로 8 KiB 이하가 되고, 머리·청크 틀이 나머지 56 KiB
안이면 상한 너머 본문은 판과 무관하게 본문 상한이 먼저 판정한다. 예산이 없거나 풀린 본문(admin
성공 본문의 `lift`)은 묶지 않는다. 문서화된 수(1,114,112 · 116,736 · 65,536)는 그대로다.
기각 — 틀 몫을 버퍼 크기만큼 늘리기(문서화된 수와 교차 언어 65,536 이 바뀐다) · http.client 의
버퍼 갈아 끼우기(응답의 `fp` 를 바꾼다 — 침습적).

시험 — 타이밍에 기대지 않는 RED 둘:
- test_wire.py::test_a_budgeted_read_asks_for_8_kib_however_large_the_buffer — 131,072 버퍼를 늘
  꽉 채우는 가짜 원문 읽기: 예산 아래 첫 읽기는 8,192 · `lift` 뒤와 예산 없는 교환은 131,072.
- test_token_response_cap.py::test_the_body_cap_wins_over_buffer_read_ahead[레인 다섯 × 길이·청크] —
  `makefile()` 기본 버퍼 131,072(3.14 와 같게) · 클라이언트 응답의 원문 읽기는 늘 꽉 참(서버가
  한참 앞섬) · 첫 조각을 상한 512 바이트 아래까지 늘려 버퍼가 빈 청크 경계를 상한 바로 아래에
  둔다. 수정 전에는 그 경계의 채우기가 예산 + 1 에 닿는다. 길이 레인은 대조군.

실측(RED → GREEN):
- 로컬 Windows 3.13.11: 수정 전 20 회 모두 같은 6 실패(청크 다섯 + 화이트박스 — 120/120)·길이 다섯
  통과, 수정 뒤 20 회 모두 11 통과.
- Docker(LF 트리, 새 시험 둘 × 3 회): 3.10.22 · 3.11.18 · 3.12.16 · 3.13.18 · 3.14.10 모두 수정 전
  「6 failed, 5 passed」 3/3 → 수정 뒤 「11 passed」 3/3.
- 원래 시험, 실 3.14.10 Docker 30 회 반복(-k test_a_huge_body_is_refused_holding_only_what_was_read):
  수정 전 30 회 중 12 회 실패 — 청크 사례 150 개 중 16 실패(admin 5 · cc 3 · code 3 · refresh 3 ·
  introspect 2), 길이 사례 150 개 중 0. 수정 뒤 30 회 중 0 회 — 30 회 모두 「10 passed」.
  (같은 반복을 CRLF 스냅숏에서 먼저 돌렸을 때 수정 전 9/30 회 · 14/150.)

게이트 — 로컬 3.13.11(워크트리의 새 venv, keycloak_sdk 를 워크트리에서 import): pytest -m "not
integration" --cov=keycloak_sdk 1545 passed · 커버리지 100.00% · ruff check · ruff format --check ·
mypy src(strict) · check-docs --strict rc=0. Docker 3.14.10(CI 단계 그대로 — `--only-binary :all:`
설치 · urllib3 2.8.0 · requests 2.34.2): ruff check · ruff format --check · 1545 passed · 커버리지
100.00% · mypy src.

문서 — wire.py 머리말에 ⚠️ 한 절(묶음과 이유), `.claude/rules/python.md` Gotchas 에 한 줄(3.14 버퍼
선읽기 → 시험 포인터). 예산 4,690 B 는 올리지 않고 같은 파일에서 바꿨다(적재 4,676 → 4,686 B):
`_internal/` 예시 나열(추출 가능한 사실 — 지금 16 모듈 중 셋) · py.typed 측정(2 → 0)을 되풀이한 구
하나 · 리다이렉트 절에서 앞 문장을 되풀이한 두 문장.

[no-changelog: 미게시 결함 — 바이트 예산은 py-v1.0.6 에 없었고 1.0.7 은 아직 게시 전이라 소비자에게 보이는 변화가 없다]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4jYS4tpj7usahf8MK5dXo
@sonarqubecloud

Copy link
Copy Markdown

@xzawed

xzawed commented Oct 31, 2026

Copy link
Copy Markdown
Owner Author

PM 재측정(구현자와 다른 실행):

계측 수정 전(부모 wire.py) 수정 후
로컬 3.13.11, 새 결정적 시험 6 failed, 5 passed 11 passed
실제 Python 3.14.10(Docker, io.DEFAULT_BUFFER_SIZE 131072), 원래 시험 -k huge 15 회 5/15 회 실패(7 케이스) 0/15 회 (새 시험 포함 20 케이스 × 15)
CI #916 의 3.14 레인 두 시도 모두 실패 3.10 · 3.11 · 3.12 · 3.13 · 3.14 통과

경계 판정(코드): charge 가 used > cap 에서 곧바로 던지므로 성공한 충전 뒤 room() ≥ 1(0·음수 불가) · lift() 뒤 room() 은 None(묶지 않음) · readinto 는 자른 버퍼보다 많이 돌려줄 수 없어 한 번의 충전 ≤ 8,192. Grok 레그는 네 번(설계 질문 셋 · 닫힌 코드 질문 하나) 시간 초과로 미수행.

빨강 둘은 php-mirror(미러에 v1.5.1 태그가 없다) — PHP 미러 토큰(사람) 교체 전까지 설계상 빨갛다.

@xzawed
xzawed merged commit 497326d into main Oct 31, 2026
30 of 32 checks passed
@xzawed
xzawed deleted the fix/python-wire-readahead-314 branch October 31, 2026 06:44
xzawed added a commit that referenced this pull request Oct 31, 2026
step 8 확인 뒤(node scripts/check-registry-truth.mjs, 2026-10-31): LIVE python keycloak-sdk 1.0.7.
게시 바이트 ↔ 태그(내려받아 대조, autocrlf 끈 git archive — scratch bytes.sh): wheel · sdist 의 keycloak_sdk/
== git archive py-v1.0.7 python/src/keycloak_sdk(42 파일 — #925 의 3.14 수정 포함). 선행 조건: python 은 태그만 — 있다.
python 은 3.14 회귀(#925) 수정 뒤에 요청했다(#926 → py-v1.0.7 = 9b68dd7). node·.NET·ruby 는 #924.
java · kotlin(Portal Publish) · php(미러 토큰)는 게시 뒤에 따로 올린다. 틀: #863 의 python 줄.

가드: test-deploy-facts 184/0 · test-publication-claims(dash, LANG=) 241/0 · check-versions rc 0 ·
test-check-versions 99/0 · check-docs --strict rc 0. 진짜 검증은 이 PR 의 griffe 가 py-v1.0.7 과 비교하는 실행이다.


Claude-Session: https://claude.ai/code/session_01R4jYS4tpj7usahf8MK5dXo

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant