feat(docker): 휴대폰 앱 받기 경로에 앱 안 받기와 같은 상한 · 흘려보내기를 둔다

APK 받기가 일반 /api 로 가면 X-Accel-Buffering 이 외부 프록시로 넘어가지 않아 받는 사람마다 임시 파일이 쌓이고,
버퍼링을 끈 받기에 둘 동시 연결 상한도 없다(리뷰). 앱 안 대용량 받기와 같은 location(대소문자를 가리지 않는 정규식 ·
같은 그릇)을 두고, CI 의 받기 경로 시험에 변형 · 정보 경로 · 그릇 공유를 더했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 23:29:05 +09:00
parent e9acb7809c
commit 7c70862647
4 changed files with 59 additions and 8 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ location @api_payload_too_large {
return 200 '{"success":false,"error":{"code":"FILE_002","message":"","traceId":"$request_id"}}';
}
# 동시 연결 수 초과 429 — 대용량 받기의 limit_conn (11단계 — nginx.conf 의 http 단계 "동시 연결 수"). 백엔드의 요청 수 한도와 같은 REQ_001
# 동시 연결 수 초과 429 — 대용량 받기 · 휴대폰 앱 받기의 limit_conn (11단계 — nginx.conf 의 http 단계 "동시 연결 수"). 백엔드의 요청 수 한도와 같은 REQ_001
location @api_too_many_requests {
add_header_inherit off;
default_type application/json;
+1 -1
View File
@@ -23,7 +23,7 @@
# 받아 가면 끊지 않고, 받는 쪽이 60초 동안 하나도 받지 않으면 끊는다(브라우저 · 내려받기 관리자는 Range 로 이어 받는다 — mail/README.md 의 시험)
# - 동시 연결: 클라이언트 주소마다 10 · 공개 받기 전체 400 — 넘으면 429 REQ_001 (nginx.conf 의 http 단계 "동시 연결 수")
# - 토큰 칸은 [^/]+ 가 아니라 .+ — nginx 는 %2F 를 / 로 풀어 location 을 고르고 백엔드는 한 칸으로 읽는다(지금은 백엔드의 토큰 검사가 그 꼴을 404 로 막지만 기대지 않는다)
# - 같은 줄들이 nginx.conf 의 앱 안 받기(/api/messages/{id}/large-files/…)에 있다 — 한쪽을 고치면 다른 쪽도
# - 같은 줄들이 nginx.conf 의 앱 안 받기(/api/messages/{id}/large-files/…) · 휴대폰 앱 받기(/api/app-releases/…/file)에 있다 — 한쪽을 고치면 다른 쪽도
# error_page 를 하나라도 쓰면 위의 것을 물려받지 않아 셋을 다 적는다
location ~* ^/api/public-links/.+/files/ {
limit_conn download_public_ip 10;
+31 -3
View File
@@ -131,13 +131,17 @@ map $host $page_csp {
# ----------------------------------------
# 대용량 첨부 받기의 동시 연결 수 (11단계) — 공개 받기(/api/public-links/<토큰>/files/…) · 앱 안 받기(/api/messages/<id>/large-files/…)
# 휴대폰 앱(APK) 받기(/api/app-releases/<플랫폼>/latest/file — 2026-11-08, 계약 1절 "휴대폰 앱 받기")도 같은 꼴로 흘려보내며 앱 안 받기의 그릇을 함께 쓴다
#
# 받기는 버퍼링을 끄고 흘려보낸다 — 받는 쪽이 느리면 연결 하나가 nginx 연결 둘(클라이언트 · 백엔드)과 백엔드의 파일을 그동안 잡는다.
# 토큰을 가진 사람이 느린 받기를 수백 개 열어 이 nginx(포털과 함께)를 멈추지 못하게 센다 — 넘으면 429 → @api_too_many_requests 의 REQ_001 JSON:
# 공개 받기 클라이언트 주소마다 10 · 공개 받기 전체 400
# 앱 안 받기 클라이언트 주소마다 30(사무실 NAT) · 앱 안 받기 전체 200
# 앱 안 받기 클라이언트 주소마다 30(사무실 NAT) · 앱 안 받기 전체 200 — 휴대폰 앱 받기도 여기서 함께 센다
# 전체 한도를 둘로 나눈 까닭 — 하나를 함께 쓰면 바깥 주소 몇십 개(IPv6 면 /64 하나)로 공개 받기가 그릇을 채워 직원의 앱 안 받기까지 모두 429 가 된다.
# 세는 것은 처리 중인 요청이다(쉬는 keep-alive 연결은 세지 않는다). 두 location 모두 대소문자를 가리지 않는다(위 머리말).
# 휴대폰 앱 받기를 따로 세지 않는 까닭 — 앱 안 받기처럼 포털에 로그인한 직원의 받기라 막으려는 것(느린 받기 여러 개로 그릇 채우기)이 같고,
# 그릇(받기 600 개 — 아래 worker_connections 셈)을 늘리지 않는다. APK(30MB 남짓)는 판이 나올 때 직원마다 한 번 받는다 — 사무실 휴대폰이 한꺼번에 받다
# 대용량 받기와 합쳐 주소마다 30 에 닿으면 넘은 받기만 429(잠시 뒤 다시)다.
# 세는 것은 처리 중인 요청이다(쉬는 keep-alive 연결은 세지 않는다). 세 location 모두 대소문자를 가리지 않는다(위 머리말).
#
# 클라이언트 주소 — 이 nginx 앞에는 늘 외부 리버스 프록시가 있어 $remote_addr 은 프록시다. 프록시는 X-Forwarded-For 에 자기가 본
# 클라이언트 주소를 이어 붙인다(docker 스킬 "외부 리버스 프록시 요구사항") — 그 마지막 칸을 쓴다. 머리글이 없으면(프록시 없이 바로) 접속 주소.
@@ -327,7 +331,7 @@ server {
include /etc/nginx/snippets/share-api-${NGINX_PORTAL_SHARE}.conf;
# 대용량 첨부 받기 GET·HEAD /api/messages/{id}/large-files/{fileId} (11단계 — 계약 4절 "우리 웹메일에서 읽기") — 공개 받기와 같게 흘려보낸다
# (nginx-share-api-open.conf 의 파일 받기와 같은 줄들 — 한쪽을 고치면 다른 쪽도):
# (nginx-share-api-open.conf 의 파일 받기 · 아래 휴대폰 앱 받기와 같은 줄들 — 한쪽을 고치면 다른 쪽도):
# - 동시 연결은 클라이언트 주소마다 30(사무실 NAT) · 앱 안 받기 전체 200 (위 http 단계). 넘으면 429 REQ_001
# - 버퍼링 · 압축을 끄고 X-Accel-Buffering 을 넘긴다 — 외부 프록시(NPM)도 이 응답을 버퍼에 쌓지 않는다(받는 사람마다 1GB 를 디스크에 쌓지 않게).
# Range 는 손대지 않고 넘긴다(206 그대로)
@@ -352,6 +356,30 @@ server {
proxy_pass $backend_upstream;
}
# 휴대폰 앱(APK) 받기 GET·HEAD /api/app-releases/android/latest/file (2026-11-08 — 계약 1절 "휴대폰 앱 받기": 로그인한 직원 · 판마다 30MB 남짓 · Range 206) —
# 머리글이 대용량 첨부 받기와 같아(backend shared/utils/download-headers.ts — X-Accel-Buffering: no) 위 앱 안 받기와 같은 줄들로 흘려보낸다(한쪽을 고치면 다른 쪽도):
# - 동시 연결은 앱 안 받기의 그릇(download_app_ip 30 · download_app_total 200)을 함께 쓴다 — 까닭은 http 단계 "동시 연결 수". 넘으면 429 REQ_001
# - 버퍼링 · 압축을 끄고 X-Accel-Buffering 을 넘긴다 — 외부 프록시(NPM)도 받는 사람마다 APK 를 임시 파일에 받아 두지 않는다
# - 정보 GET /api/app-releases/android/latest(JSON)는 /file 이 없어 이 location 에 들지 않는다 — 위 /api/ 그대로
# - 대소문자를 가리지 않는다(머리말). 플랫폼 · 판 칸은 .+ — nginx 는 %XX 를 풀고(%2F 도) 겹 / · .. 를 정리한 뒤 location 을 고른다.
# 끝은 묶지 않는다 — 백엔드가 받는 꼴(끝의 / 등)이 비켜 가지 않게. 없는 이웃 경로가 들어와도 상한 · 흘려보내기가 더해질 뿐이다
# 공개 내려받기 server 의 /api/ 는 공개 API 밖을 모두 404 RES_001 로 닫는다 — 이 경로도 거기서는 백엔드에 가지 않는다(백엔드도 공개 호스트에서 막는다)
# error_page 를 하나라도 쓰면 위의 것을 물려받지 않아 셋을 다 적는다
location ~* ^/api/app-releases/.+/file {
limit_conn download_app_ip 30;
limit_conn download_app_total 200;
error_page 413 @api_payload_too_large;
error_page 429 @api_too_many_requests;
error_page 500 502 503 504 @api_unavailable;
proxy_buffering off;
proxy_buffer_size 16k;
proxy_busy_buffers_size 16k;
gzip off;
proxy_pass_header X-Accel-Buffering;
proxy_read_timeout 90s;
proxy_pass $backend_upstream;
}
# 대용량 첨부의 조각 올리기 PUT /api/file-uploads/{id}/chunks/{n} (11단계 — 계약 4절 "올리기") — 조각 16MiB(계약 상수 — 마지막 조각만 짧다)
# + 여유 = 17m 고정. 계약 상수라 env 로 바꾸지 않는다(바꾸려면 계약 · 백엔드 · 화면을 함께). 그 밖의 /api/file-uploads(만들기 · 끝내기 · 상태 · 지우기)는
# JSON 이라 기본 1m 그대로다. 요청 본문은 nginx 가 다 받아 둔 뒤(proxy_request_buffering 기본 켬 — 느린 연결은 nginx 가 받는다, /tmp/client_temp)