From adf4dd75450603b0b171066200497059632f952d Mon Sep 17 00:00:00 2001 From: Sanjin <102841251+duckduckgrayduck@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:18:44 -0500 Subject: [PATCH 1/4] Add a size cap to addon_events_patches --- config/nginx.conf.erb | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index ed9c8f15..5ce263c1 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -10,8 +10,22 @@ events { http { - map_hash_bucket_size <%= ENV.fetch("NGINX_HASH_BUCKET_SIZE", 64) %>; + <% cors_preflight = <<~'NGINX' + # Answer CORS preflights directly from nginx + if ($request_method = 'OPTIONS' ) { + add_header "Access-Control-Allow-Origin" $cors_origin_header always; + add_header "Access-Control-Allow-Credentials" $cors_cred; + add_header "Access-Control-Allow-Methods" $cors_methods; + add_header "Access-Control-Allow-Headers" $cors_headers; + add_header 'Access-Control-Max-Age' $cors_max_age; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 204; + } + NGINX + %> + map_hash_bucket_size <%= ENV.fetch("NGINX_HASH_BUCKET_SIZE", 64) %>; gzip on; gzip_http_version 1.0; gzip_comp_level 5; @@ -192,18 +206,17 @@ http { rewrite ^(.*) $scheme://$host$1 permanent; } + location /api/addon_events/ { + client_max_body_size <%= ENV.fetch("ADDON_EVENT_MAX_BODY_MB", 25) %>m; + <%= cors_preflight %> + + proxy_redirect off; + proxy_pass http://app_server; + } + location / { - if ($request_method = 'OPTIONS' ) { - add_header "Access-Control-Allow-Origin" $cors_origin_header always; - add_header "Access-Control-Allow-Credentials" $cors_cred; - add_header "Access-Control-Allow-Methods" $cors_methods; - add_header "Access-Control-Allow-Headers" $cors_headers; - add_header 'Access-Control-Max-Age' $cors_max_age; - add_header Content-Length 0; - add_header Content-Type text/plain; - return 204; - } + <%= cors_preflight %> proxy_redirect off; proxy_pass http://app_server; From ffe30c0e369566dda7ee16fd462a253d50539002 Mon Sep 17 00:00:00 2001 From: Sanjin <102841251+duckduckgrayduck@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:19:50 -0500 Subject: [PATCH 2/4] Return a 429 when someone surpasses the rate limit, because nginx defaults to 503 --- config/nginx.conf.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index 5ce263c1..c1072fdb 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -95,7 +95,7 @@ http { } limit_req_zone $limit_key zone=one:10m rate=10r/s; - + limit_req_status 429; log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id'; log_format compression '$remote_addr - $remote_user [$time_local] ' '"$request" $status $bytes_sent ' From 69bd5bf7f11ddac2586028aa46642af847952a80 Mon Sep 17 00:00:00 2001 From: Sanjin <102841251+duckduckgrayduck@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:17:01 -0500 Subject: [PATCH 3/4] Set sitewide 25MB cap, bump keepalive_timeout to 95 to stop getting H18s, add Retry-After and CORS headers to nginx-generated 429s so the frontend can read them and clients can back off --- config/nginx.conf.erb | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index c1072fdb..d98e08bf 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -109,7 +109,8 @@ http { #Must read the body in 5 seconds. client_body_timeout 5; - client_max_body_size 4G; + # Uploads go direct to S3 and Mailgun attachments are fetched by URL; 25MB covers GitHub webhooks + client_max_body_size <%= ENV.fetch("NGINX_MAX_BODY_MB", 25) %>m; upstream app_server { server unix:/tmp/nginx.socket fail_timeout=0; @@ -196,22 +197,41 @@ http { <% end %> } + # Headers for nginx's own 429s only; responses from Django set their own + map "$upstream_status:$status" $retry_after { + default ""; + ":429" 1; + } + + map "$upstream_status:$status" $limit_cors_origin { + default ""; + ":429" $cors_origin_header; + } + + map "$upstream_status:$status" $limit_cors_cred { + default ""; + ":429" $cors_cred; + } + + map "$upstream_status:$status" $limit_cors_expose { + default ""; + ":429" "Retry-After"; + } + server { listen <%= ENV["PORT"] %>; server_name _; - keepalive_timeout 5; + keepalive_timeout 95; limit_req zone=one burst=20 nodelay; - if ($http_host ~ "\.$" ){ - rewrite ^(.*) $scheme://$host$1 permanent; - } - location /api/addon_events/ { - client_max_body_size <%= ENV.fetch("ADDON_EVENT_MAX_BODY_MB", 25) %>m; - <%= cors_preflight %> + add_header Retry-After $retry_after always; + add_header Access-Control-Allow-Origin $limit_cors_origin always; + add_header Access-Control-Allow-Credentials $limit_cors_cred always; + add_header Access-Control-Expose-Headers $limit_cors_expose always; - proxy_redirect off; - proxy_pass http://app_server; + if ($http_host ~ "\.$" ){ + rewrite ^(.*) $scheme://$host$1 permanent; } location / { @@ -224,4 +244,4 @@ http { } -} +} \ No newline at end of file From 9cdc4841ac123100485be7beb5afa7b74ab0c27d Mon Sep 17 00:00:00 2001 From: Sanjin <102841251+duckduckgrayduck@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:19:41 -0500 Subject: [PATCH 4/4] Inline the cors preflight to shrink the PR and restore its old setup --- config/nginx.conf.erb | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index d98e08bf..a8d1d5dd 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -10,21 +10,6 @@ events { http { - <% cors_preflight = <<~'NGINX' - # Answer CORS preflights directly from nginx - if ($request_method = 'OPTIONS' ) { - add_header "Access-Control-Allow-Origin" $cors_origin_header always; - add_header "Access-Control-Allow-Credentials" $cors_cred; - add_header "Access-Control-Allow-Methods" $cors_methods; - add_header "Access-Control-Allow-Headers" $cors_headers; - add_header 'Access-Control-Max-Age' $cors_max_age; - add_header Content-Length 0; - add_header Content-Type text/plain; - return 204; - } - NGINX - %> - map_hash_bucket_size <%= ENV.fetch("NGINX_HASH_BUCKET_SIZE", 64) %>; gzip on; gzip_http_version 1.0; @@ -236,7 +221,17 @@ http { location / { - <%= cors_preflight %> + # Answer CORS preflights directly from nginx + if ($request_method = 'OPTIONS' ) { + add_header "Access-Control-Allow-Origin" $cors_origin_header always; + add_header "Access-Control-Allow-Credentials" $cors_cred; + add_header "Access-Control-Allow-Methods" $cors_methods; + add_header "Access-Control-Allow-Headers" $cors_headers; + add_header 'Access-Control-Max-Age' $cors_max_age; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 204; + } proxy_redirect off; proxy_pass http://app_server;