Hello Community!!
I’m working on a project where I’m using a QUIC-based reverse proxy (implemented with the quic-go library) to forward chunked data uploads to AWS S3 pre-signed URLs. Here’s an overview of my setup, goals, and the issues I’m facing:
Setup Server:
A custom HTTP/3 QUIC server listens on a specific endpoint (e.g., /post-reverse) to receive PUT requests with chunked data. The request contains: Chunked data in the body. A custom header (X-Presigned-URL) with the target S3 pre-signed URL. Upon receiving the request: The server extracts the X-Presigned-URL from the headers. It forwards the request body to the pre-signed URL using a reverse proxy mechanism. It streams the response from S3 back to the client.
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"time"
"github.com/Private-repo/go-httperr"
"github.com/Private-repo/go-reqlog"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/quic-go/qlog"
)
const (
Host = "0.0.0.0"
Port = 4242
webServerShutdownTimeout = 5 * time.Second
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
mux := http.NewServeMux()
// V1 APIs
mux.Handle("/post-reverse", httperr.HandlerFunc(ReverseProxy))
hdlr := reqlog.RequestLogger(mux, nil)
addr := fmt.Sprintf("%s:%d", Host, Port)
server := http3.Server{
Handler: hdlr,
Addr: addr,
QUICConfig: &quic.Config{
Tracer: qlog.DefaultConnectionTracer,
},
}
go func() {
if err := server.ListenAndServeTLS("server.crt", "server.key"); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.ErrorContext(ctx, "error starting server", "error", err)
os.Exit(1)
}
}()
slog.InfoContext(ctx, "started UDP-Srv", "addr", addr)
<-ctx.Done()
// Shutdown gracefully.
ctx, cancel = context.WithTimeout(context.Background(), webServerShutdownTimeout)
defer cancel()
slog.InfoContext(ctx, "shutting down")
if err := server.Shutdown(ctx) ; err != nil {
slog.ErrorContext(ctx, "http server shutdown error", "error", err)
}
}
func ReverseProxy(w http.ResponseWriter, r *http.Request) error {
slog.Info("ReverseProxy called", "method", r.Method, "path", r.URL.Path)
if r.Method != http.MethodPut {
slog.Warn("Method not allowed", "method", r.Method)
return httperr.Errorf(http.StatusMethodNotAllowed, "Method not allowed")
}
// Extract Pre-Signed URL
presignedURL := r.Header.Get("X-Presigned-URL")
if presignedURL == "" {
slog.Warn("Missing X-Presigned-URL header")
return httperr.Errorf(http.StatusBadRequest, "Missing X-Presigned-URL header")
}
// Validate Pre-Signed URL
url, err := url.Parse(presignedURL)
if err != nil {
slog.Warn("Invalid X-Presigned-URL header", "error", err)
return httperr.Errorf(http.StatusBadRequest, "Invalid X-Presigned-URL header")
}
slog.Info("Using Pre-Signed URL", "url", presignedURL)
// Configure reverse proxy
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.Director = func(req *http.Request) {
req.URL = url
req.Host = url.Host
req.Method = r.Method
req.Header = r.Header.Clone() // Clone headers
req.Header.Del("X-Presigned-URL")
req.ContentLength = r.ContentLength
}
// Handle proxy errors
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
slog.Error("Proxy error", "error", err)
http.Error(rw, "Proxy error: "+err.Error(), http.StatusBadGateway)
}
// Serve the proxied request
slog.Info("Forwarding request to S3")
proxy.ServeHTTP(w, r)
return nil
}
Client:
The client sends chunked data via HTTP/3 using the quic-go client. Each request contains the X-Presigned-URL header with the S3 URL and the chunk payload. It sends 8MB chunk
func (lu *Uploader) sendChunk(client *http.Client, chunkObject models.ChunkUrl, assetPath string, subtitleRelativeMap *sync.Map) error {
var err error
var dataReader io.Reader
for attempt := 0; attempt < 4; attempt++ {
if strings.Contains(assetPath, ".tar") {
lu.logger.Info("path", "path", assetPath, "offset", chunkObject.Offset, "size", chunkObject.Size)
path := strings.TrimSuffix(assetPath, ".tar")
lu.logger.Info("create.tar", "path", filepath.Base(path))
dataReader, err = lu.createTar(path, subtitleRelativeMap)
if err != nil {
return err
}
_, err = io.CopyN(io.Discard, dataReader, chunkObject.Offset)
if err != nil {
return err
}
dataReader = io.LimitReader(dataReader, chunkObject.Size)
} else {
var file *os.File
file, err = os.Open(assetPath)
if err != nil {
return err
}
defer file.Close()
dataReader = io.NewSectionReader(file, chunkObject.Offset, chunkObject.Size)
}
req, err := http.NewRequest(http.MethodPut, "https://localhost:4242/post-reverse", dataReader)
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/octet-stream")
req.Header.Add("X-Presigned-URL", chunkObject.UploadUrl)
req.ContentLength = chunkObject.Size
resp, err := client.Do(req)
if err != nil {
lu.logger.Error("upload.chunk.error", "attempt", attempt, "err", err)
continue
}
// handle empty response
responseBody, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
lu.logger.Error("upload.chunk.error", "attempt", attempt, "response", string(responseBody))
time.Sleep(2 * time.Second)
continue
}
// SUCCESS
lu.logger.Info("upload.chunk.success", "path", assetPath, "offset", chunkObject.Offset, "size", chunkObject.Size)
return nil
}
return err
}
func makeOptimizedClient() *http.Client {
tr := &http3.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
QUICConfig: &quic.Config{},
}
defer tr.Close()
client := &http.Client{
Transport: tr,
Timeout: 10 * time.Minute,
}
return client
}
my Goals are
Enable efficient chunked uploads to S3 using a reverse proxy that leverages QUIC for low-latency data transfer. Ensure successful forwarding of chunked data from the client to the S3 pre-signed URL via the reverse proxy. Provide proper responses (e.g., HTTP 200 for successful uploads or error codes for issues) to the client after the S3 upload.
Issues: Failed Proxy to S3:
When the server forwards the chunked request to the S3 pre-signed URL All i get is 502 Bad Gateway with the error: http3: parsing frame failed: timeout: no recent network activity.
I tried using the same code with a basic HTTP client and server, and it works fine with TCP connections. However, when I switch to QUIC implementation, it starts throwing errors.
Please help, and feel free to ask for clarification if my question is unclear.
Top comments (2)
server := http3.Server{
Handler: hdlr,
Addr: addr,
QUICConfig: &quic.Config{
Tracer: qlog.DefaultConnectionTracer,
},
}
try adding Keep alive header. Also I found some timeout headers to prevent early timeout.
server := http3.Server{
Handler: hdlr,
Addr: addr,
QUICConfig: &quic.Config{
Tracer: qlog.DefaultConnectionTracer,
KeepAlive: true, // Enable keep-alive to prevent network inactivity timeouts
HandshakeIdleTimeout: 10 * time.Second,
MaxIdleTimeout: 30 * time.Second,
},
}
Tried this approach didn't work.