v1.11.14
Relio v1.11.14 — 큰 MCP 요청이 "Parse error" 로 돌아오던 문제
1MB 를 넘는 MCP 요청을 보내면 클라이언트가 올바르게 만든 JSON-RPC 메시지가 파싱 오류로 거절되던 문제를 고쳤습니다. 이제 크기 문제라는 사실이 응답에 그대로 드러납니다.
1. 원인
1-1. 상한까지만 읽고 멈추면 본문이 잘린다
MCP 엔드포인트는 본문을 이렇게 읽었습니다.
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
s.writeErrorStatus(w, http.StatusBadRequest, nil, -32700, "Parse error", nil)
return
}io.LimitReader 는 상한을 넘는 입력을 오류로 만들지 않습니다. 딱 그 지점까지 읽고 정상적으로 EOF 를 돌려줍니다. 그래서 1MB 를 넘는 본문은 오류 없이 1MB 에서 잘린 채 다음 단계로 넘어갔습니다.
1-2. 잘린 JSON 은 깨진 JSON 이다
잘려 나간 본문은 닫히지 않은 문자열과 중괄호로 끝나므로 parseRequests 가 반드시 실패합니다. 그 결과 클라이언트가 받는 답은 이것이었습니다.
{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}HTTP 상태는 400 입니다. 클라이언트 입장에서 이 답은 자기 직렬화가 잘못됐다는 뜻이므로, 정작 문제인 "본문이 상한을 넘었다" 는 사실은 어디에도 없습니다. 긴 노트 본문이나 큰 arguments 를 담은 tools/call 이 이유 없이 실패하는 것처럼 보이고, 같은 메시지를 그대로 재시도하면 똑같이 실패합니다.
REST 쪽은 이미 이 상황을 구분해서 답하고 있었습니다. httpx.DecodeJSON 은 http.MaxBytesReader 로 413 request_too_large 를 돌려주고, 멱등성 미들웨어는 상한보다 한 바이트 더 읽어 초과를 판별합니다. MCP 만 이 구분이 빠져 있었습니다.
2. 수정
상한보다 한 바이트 더 읽어, 잘린 본문과 원래 깨진 본문을 구분합니다.
// MaxRequestBytes caps one MCP HTTP message.
const MaxRequestBytes = 1 << 20
func readRequestBody(r io.Reader) (body []byte, tooLarge bool, err error) {
body, err = io.ReadAll(io.LimitReader(r, MaxRequestBytes+1))
if err != nil {
return nil, false, err
}
if len(body) > MaxRequestBytes {
return nil, true, nil
}
return body, false, nil
}초과한 본문은 413 과 함께 JSON-RPC 오류로 답하고, 지켜야 할 상한을 data 에 담아 보냅니다.
{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"요청 본문이 너무 큽니다.","data":{"maxBytes":1048576}}}읽기 자체가 실패한 경우(연결 끊김 등)는 지금까지처럼 400 Parse error 입니다. 상한 값 자체는 1MB 그대로이며, 달라진 것은 상한을 넘겼을 때 돌아오는 답뿐입니다.
3. 회귀 방지
- 상한과 정확히 같은 크기의 본문은 통과하고, 한 바이트만 넘어도 초과로 판정되는지.
- 상한을 넘긴 실제
tools/call메시지가 초과로 판정되는지, 그리고 그것을 상한에서 자른 본문은parseRequests가 반드시 거절하는지 — 잘린 본문이 우연히 파싱되면 일부만 담긴 호출이 실행될 수 있으므로 함께 확인합니다. - 읽기 오류는 초과가 아니라 오류로 전달되는지.
- 413 응답이 JSON-RPC 형식을 유지하며
data.maxBytes로 상한을 알려주는지.
4. 적용
마이그레이션은 없습니다. 재시작하면 바로 적용되며, 1MB 이하의 요청은 동작이 전혀 달라지지 않습니다.
오프라인 이미지
| 항목 | 값 |
|---|---|
| Asset | relio-v1.11.14.tar.gz |
| Docker Image | relio:v1.11.14 |
| SHA-256 | 45b56a8586c7634e9101b4e4ef82b69a857d687e4e0d1f269fa0e0ca2b4671f6 |
| 압축 크기 | 36M |
| Architecture | linux/amd64 |
이 Release가 직접 제공하는 Asset은 위 Docker Image tar.gz 하나뿐입니다.
오프라인 서버에서 로드
gunzip -c relio-v1.11.14.tar.gz | docker load
docker image inspect relio:v1.11.14실행
# 최초 1회만 생성하고 비밀번호 관리 도구에 보관하세요.
ENCRYPTION_KEY="$(openssl rand -hex 32)"
docker run -d \
--name relio \
-p 8080:8080 \
-e POSTGRES_DSN="postgres://relio:password@postgres:5432/relio" \
-e BOOTSTRAP_ADMIN="admin" \
-e BOOTSTRAP_ADMIN_PASSWORD="ChangeMe-Immediately" \
-e ENCRYPTION_KEY="$ENCRYPTION_KEY" \
-v relio-data:/var/lib/relio \
relio:v1.11.14Relio Application이 받는 환경변수는 필수 3개(POSTGRES_DSN, BOOTSTRAP_ADMIN, BOOTSTRAP_ADMIN_PASSWORD)와 선택 1개(ENCRYPTION_KEY)뿐입니다.
ENCRYPTION_KEY를 설정하면 Personal Key와 SSO Client Secret이 재기동, 이미지 교체, relio-data Volume 재생성 후에도 유지됩니다. 설정하지 않으면 자격증명을 여는 Key가 Volume 안에만 존재하므로 /var/lib/relio를 PostgreSQL과 같은 복구 시점으로 반드시 보존해야 합니다.
검증
Release 전에 Source Test, TypeScript Typecheck, Frontend Build, Go Vet, 환경변수 계약, 외부 정적 Asset 차단, 비 Root 실행, 완전 격리 Network 신규 설치 Smoke Test, 이전 Release 업그레이드 Test, Docker Save/Load 재검증을 통과합니다.