Add HTTP upload idempotency support
This commit is contained in:
@@ -29,9 +29,12 @@ type uploadAcceptedResponse struct {
|
||||
}
|
||||
|
||||
type httpErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Error string `json:"error"`
|
||||
Retryable bool `json:"retryable,omitempty"`
|
||||
}
|
||||
|
||||
const idempotencyKeyHeader = "Idempotency-Key"
|
||||
|
||||
func newUploadHTTPHandler(ctx context.Context, cfg config.Config, environment config.Environment) (http.Handler, error) {
|
||||
config.ApplyDefaults(&cfg)
|
||||
tokens, err := resolveUploadTokens(cfg, environment)
|
||||
@@ -98,14 +101,16 @@ func (handler uploadHTTPHandler) handleUpload(w http.ResponseWriter, r *http.Req
|
||||
writeHTTPError(w, http.StatusUnsupportedMediaType, "unsupported content type")
|
||||
return
|
||||
}
|
||||
if !handler.coordinator.CanAccept() {
|
||||
writeHTTPError(w, http.StatusServiceUnavailable, "upload queue is full")
|
||||
idempotencyKey, err := uploadIdempotencyKey(r.Header)
|
||||
if err != nil {
|
||||
writeHTTPError(w, http.StatusBadRequest, "invalid idempotency key")
|
||||
return
|
||||
}
|
||||
record, err := handler.coordinator.Submit(r.Context(), UploadRequest{
|
||||
PipelineID: pipelineID,
|
||||
ContentType: contentType,
|
||||
Body: r.Body,
|
||||
PipelineID: pipelineID,
|
||||
ContentType: contentType,
|
||||
Body: r.Body,
|
||||
IdempotencyKey: idempotencyKey,
|
||||
})
|
||||
if err != nil {
|
||||
writeUploadSubmitError(w, err)
|
||||
@@ -144,10 +149,44 @@ func (handler uploadHTTPHandler) authenticate(header string) (string, bool) {
|
||||
return pipelineID, ok
|
||||
}
|
||||
|
||||
func uploadIdempotencyKey(header http.Header) (string, error) {
|
||||
values := header.Values(idempotencyKeyHeader)
|
||||
if len(values) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if len(values) != 1 {
|
||||
return "", fmt.Errorf("idempotency key must appear at most once")
|
||||
}
|
||||
key := values[0]
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("idempotency key is required when header is present")
|
||||
}
|
||||
if len(key) > 128 {
|
||||
return "", fmt.Errorf("idempotency key must be at most 128 bytes")
|
||||
}
|
||||
for index := 0; index < len(key); index++ {
|
||||
character := key[index]
|
||||
if character >= 'a' && character <= 'z' ||
|
||||
character >= 'A' && character <= 'Z' ||
|
||||
character >= '0' && character <= '9' ||
|
||||
character == '.' ||
|
||||
character == '_' ||
|
||||
character == '-' ||
|
||||
character == ':' {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("idempotency key contains unsupported character")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func writeUploadSubmitError(w http.ResponseWriter, err error) {
|
||||
var idempotencyConflict UploadIdempotencyConflictError
|
||||
switch {
|
||||
case IsUploadQueueFull(err):
|
||||
writeHTTPError(w, http.StatusServiceUnavailable, "upload queue is full")
|
||||
case errors.As(err, &idempotencyConflict):
|
||||
writeHTTPErrorRetryable(w, http.StatusConflict, idempotencyConflict.Error(), idempotencyConflict.Retryable)
|
||||
case errors.Is(err, ingest.ErrUploadTooLarge):
|
||||
writeHTTPError(w, http.StatusRequestEntityTooLarge, "upload exceeds maximum size")
|
||||
case errors.Is(err, ingest.ErrUnsupportedContentType):
|
||||
@@ -158,7 +197,11 @@ func writeUploadSubmitError(w http.ResponseWriter, err error) {
|
||||
}
|
||||
|
||||
func writeHTTPError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, httpErrorResponse{Error: message})
|
||||
writeHTTPErrorRetryable(w, status, message, false)
|
||||
}
|
||||
|
||||
func writeHTTPErrorRetryable(w http.ResponseWriter, status int, message string, retryable bool) {
|
||||
writeJSON(w, status, httpErrorResponse{Error: message, Retryable: retryable})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||
|
||||
Reference in New Issue
Block a user