Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const requestBodyLimit int64 = 8 << 20
|
|
|
|
// withRequestBodyLimit must run inside authMiddleware. Reading the complete
|
|
// bounded body before dispatch prevents a valid JSON prefix from committing a
|
|
// mutation before a later size/read error is discovered. Content-Type is not a
|
|
// security boundary; CSV retains its additional row/field checks in its handler.
|
|
func withRequestBodyLimit(next http.Handler) http.Handler {
|
|
return withBodyLimitAndConcurrency(requestBodyLimit, requestBodyConcurrency(), next)
|
|
}
|
|
|
|
func withBodyLimit(limit int64, next http.Handler) http.Handler {
|
|
return withBodyLimitAndConcurrency(limit, 4, next)
|
|
}
|
|
|
|
func requestBodyConcurrency() int {
|
|
n, err := strconv.Atoi(strings.TrimSpace(os.Getenv("EVOBGP_API_BODY_MAX_CONCURRENT")))
|
|
if err == nil && n >= 1 && n <= 64 {
|
|
return n
|
|
}
|
|
return 4
|
|
}
|
|
|
|
func withBodyLimitAndConcurrency(limit int64, concurrency int, next http.Handler) http.Handler {
|
|
slots := make(chan struct{}, concurrency)
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
original := r.Body
|
|
if original != nil {
|
|
defer func() { _ = original.Close() }()
|
|
}
|
|
if r.ContentLength > limit {
|
|
writeRequestBodyTooLarge(w)
|
|
return
|
|
}
|
|
if r.Context().Err() != nil {
|
|
writeProblem(w, http.StatusRequestTimeout, "Request Timeout", "request body read cancelled")
|
|
return
|
|
}
|
|
if r.Body == nil || r.Body == http.NoBody {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
select {
|
|
case slots <- struct{}{}:
|
|
defer func() { <-slots }()
|
|
default:
|
|
w.Header().Set("Retry-After", "1")
|
|
writeProblem(w, http.StatusTooManyRequests, "Too Many Requests", "request body processing capacity is full")
|
|
return
|
|
}
|
|
bounded := http.MaxBytesReader(w, r.Body, limit)
|
|
data, err := io.ReadAll(requestContextReader{ctx: r.Context(), reader: bounded})
|
|
var tooLarge *http.MaxBytesError
|
|
if errors.As(err, &tooLarge) {
|
|
writeRequestBodyTooLarge(w)
|
|
return
|
|
}
|
|
var timeout net.Error
|
|
if r.Context().Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &timeout) && timeout.Timeout()) {
|
|
writeProblem(w, http.StatusRequestTimeout, "Request Timeout", "request body read cancelled")
|
|
return
|
|
}
|
|
if err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "could not read request body")
|
|
return
|
|
}
|
|
r.Body = io.NopCloser(bytes.NewReader(data))
|
|
r.ContentLength = int64(len(data))
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func writeRequestBodyTooLarge(w http.ResponseWriter) {
|
|
writeProblem(w, http.StatusRequestEntityTooLarge, "Content Too Large", "request body exceeds the 8 MiB limit")
|
|
}
|
|
|
|
type requestContextReader struct {
|
|
ctx context.Context
|
|
reader io.Reader
|
|
}
|
|
|
|
func (r requestContextReader) Read(p []byte) (int, error) {
|
|
if err := r.ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
return r.reader.Read(p)
|
|
}
|