fix: 문서(Pages) 저장 500 + 감사로그가 웹 화면 진입을 끊는 문제 - #4
Merged
Conversation
The three methods belonging to PageBinaryUpdateSerializer -- validate_description_binary, validate_description_html and update -- sat on PageCommentSerializer instead. The docstring left behind inside PageCommentSerializer still read "Update the page instance". PageBinaryUpdateSerializer is a plain serializers.Serializer, so DRF has no model to derive update() from. Without it, serializer.save() raises NotImplementedError and every page description save returns 500. Observed in production as 6 x `update() must be implemented` on PagesDescriptionViewSet.partial_update. Two further consequences of the misplacement, both fixed by moving the methods back: - Page saves ran no validation at all, so validate_html_content -- the HTML sanitisation that makeplane#7507 added this serializer for -- never executed. - PageCommentSerializer.update() shadowed ModelSerializer.update() and only looked for description_* keys, which PageComment does not have (it has comment_html / comment_json). Editing a page comment therefore called instance.save() without applying any validated data: the edit silently saved nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
APITokenLogMiddleware copied request and response bodies into the celery
message with no size bound, and mongo_log duplicated the whole dict, so every
message carried the payload twice. A large API response therefore published at
roughly 2x its size.
In production that reached 354,911,882 bytes against RabbitMQ's 128MB
max_message_size. RabbitMQ rejects the publish with PRECONDITION_FAILED (406)
and closes the channel -- and because the failure surfaces on the *next*
publish over that connection, the request that actually returned 500 was an
unrelated one: recent_visited_task.delay() inside ProjectViewSet.retrieve.
Users saw `GET /api/workspaces/{slug}/projects/{id}/` fail with 500 and the
sibling `/pages/` request hang until they gave up (499) -- i.e. opening a
project or a page broke because of someone else's API traffic.
Bodies are now capped at 64KB with an explicit truncation marker, bounding a
message at ~128KB.
Also in this path:
- X-Api-Key is redacted from the logged headers blob, which duplicated the raw
token for no audit value (partial PLANE-75; token_identifier is left alone
as it is the audit join key).
- StreamingHttpResponse has no .content, so page description binary downloads
raised AttributeError into log_exception on every request. Those are now
recorded as [Streaming Content] instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mote.49 added "plane.bgtasks.logger_task" to CELERY_IMPORTS. The deployed mote.50 image does not have that line, although origin/mote does -- the rest of the tuple is byte-identical. mote.50 was built from /srv/shared/app-src/plane after a stash detour (see the mote.50 warning in RELEASES-mote.md), and the one-line fix was lost in the process. The failure mode is silent by construction: an unregistered task is not an error anywhere, the worker just logs "Received unregistered task" and discards the message, so the audit trail goes to zero without anything going red. This test pins the entry and checks that every listed module actually imports. No source change is needed for that one -- rebuilding and redeploying from origin/mote is the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
도트/쿠키의 "plane 페이지가 잘 안들어가진다" 리포트에서 출발했습니다.
먼저, 정상인 것
서버 다운이 아닙니다. 컨테이너 13개 healthy, 12시간 5xx 0건, Keycloak SSO 정상(도트·쿠키 로그인 성공 로그 확인), 홈 API 전건 200, 에셋 1.3MB 33ms, 외부망 접속 정상, 헤드리스 로드 1.3초.
실제 결함은 3건이었습니다.
① 문서 본문 저장이 100% 500
PageBinaryUpdateSerializer의 메서드 3개(validate_description_binary/validate_description_html/update)가 병합 사고로PageCommentSerializer안에 들어가 있었습니다.PageCommentSerializer안에 남아 있던 docstring 이"Update the **page** instance"라고 말하는 게 결정적 증거입니다.PageBinaryUpdateSerializer는serializers.Serializer라 DRF 가update()를 유도할 모델이 없습니다. 없으면serializer.save()가NotImplementedError를 던집니다 → 저장 시도 전건 500. 운영 로그에update() must be implemented6건.메서드를 원래 클래스로 되돌리면서 부수 피해 2건도 함께 해소됩니다.
validate_html_contentHTML 살균(chore: added validation for description makeplane/plane#7507)이 실행된 적이 없습니다.update()가ModelSerializer.update()를 가렸고,description_*키만 찾는데PageComment에는 그런 필드가 없습니다(comment_html/comment_json). 그래서 페이지 댓글 수정이instance.save()만 하고 아무 값도 반영하지 않았습니다 — 조용히 저장 실패.② 감사로그가 AMQP 채널을 죽여서 화면 진입이 500
APITokenLogMiddleware가 요청·응답 본문을 무제한 복사하고,mongo_log = {**log_data, ...}가 그걸 한 번 더 복제합니다. 즉 모든 메시지가 페이로드를 2배로 싣습니다.운영에서 실측 354,911,882 바이트 가 RabbitMQ 한도(
max_message_size128MB)를 넘겼습니다. RabbitMQ 는PRECONDITION_FAILED (406)으로 publish 를 거절하면서 채널을 통째로 닫습니다. 그런데 이 실패는 같은 커넥션의 다음 publish 에서 터지기 때문에, 실제로 500 이 난 요청은 전혀 무관한 요청이었습니다 —ProjectViewSet.retrieve안의recent_visited_task.delay().사용자가 본 증상:
즉 남의 API 트래픽 때문에 내 프로젝트·문서 화면이 안 열리는 구조였습니다. "API 로 AI 랑 하는 건 되는데 웹이 안 들어가진다" 가 우연이 아니라 인과입니다.
본문을 64KB 로 캡했습니다(메시지 최대 ~128KB).
같은 경로에서 함께 처리:
X-Api-Key를headers블롭에서 마스킹 (PLANE-75 일부).token_identifier는 감사 조인키라 손대지 않았습니다.StreamingHttpResponse에는.content가 없어서 문서 바이너리 다운로드마다AttributeError가log_exception으로 들어가고 있었습니다 →[Streaming Content]로 기록.③
logger_task등록이 mote.50 에서 유실 (회귀)mote.49 에서 넣은
CELERY_IMPORTS의"plane.bgtasks.logger_task"한 줄이 배포 이미지에만 없습니다. 레포에는 있고, 튜플의 나머지는 완전히 동일합니다.mote.50 이 stash 대피/복원을 거친⚠️ 항목이 이미 경고하고 있습니다).
/srv/shared/app-src/plane에서 빌드된 결과입니다(RELEASES-mote.md 의 mote.50이건 코드 수정이 아니라 재빌드·재배포가 곧 수정입니다. 대신 재발 방지 테스트를 넣었습니다 — 미등록 태스크는 어디서도 에러가 아니고 워커가 조용히 버리기만 해서, 감사추적이 0 이 되어도 아무것도 빨개지지 않는 실패 모드이기 때문입니다.
테스트
신규 25건.
origin/mote기준선 대비 회귀 0건.실패 4건·에러 18건은 기준선과 동일합니다(에러는 컨테이너에 postgres 가 없어서 발생).
배포 시 유의
이 PR 머지 후 이미지를 새로 빌드해야 ③이 실제로 해소됩니다. 빌드소스는 반드시
origin/mote에서 깨끗하게 받아야 하고(mote.50 과 같은 사고 재발 방지), 태그 재사용 시docker inspect --format '{{.Created}}'로 실제 재빌드 여부를 확인해야 합니다.관련 없이 이미 적용 완료: 업로드 한도 200MB → 500MB (게이트웨이 nginx
client_max_body_size 500mreload,plane.envFILE_SIZE_LIMIT=524288000, proxy recreate + api×2 무중단 카나리 스왑, 300MB 실전송 검증).🤖 Generated with Claude Code