refactor: improve streaming response error handling and shorten derived IDs

- Add wroteHeaderChan to streamingResponseWriter for synchronization - Ensure Bad Gateway error is written if upstream handler fails to set headers - Truncate HMAC-SHA256 hash to 16 bytes in DeriveID for shorter base32-encoded IDs (reduces length from ~51 to ~26 characters)

lemon-mint committed Nov 3, 2025 at 16:07 UTC 7e8c94784bec5b5c74b5ef49cfe98f7318b8b4e2
2 files changed +21 -9
cmd/webclient/httpjs/http_js.go
+19 -8
@@ -429,7 +429,8 @@ func HTTPResponseToJSResponse(httpResp *http.Response) js.Value {
429 jsOptions.Set("headers", jsHeaders)
430
431 // Create and return JS Response
432 - return _Response.New(jsBody, jsOptions)
432 + jsResp := _Response.New(jsBody, jsOptions)
433 + return jsResp
434 }
435
436 // ServeHTTPAsyncWithStreaming handles an HTTP request asynchronously and returns a streaming JS Response
@@ -451,9 +452,10 @@ func ServeHTTPAsyncWithStreaming(handler http.Handler, jsReq js.Value) js.Value
452
453 // Create custom ResponseWriter that writes to pipe
454 respWriter := &streamingResponseWriter{
454 - pipeWriter: pw,
455 - header: make(http.Header),
456 - statusCode: 200,
455 + pipeWriter: pw,
456 + header: make(http.Header),
457 + statusCode: 200,
458 + wroteHeaderChan: make(chan struct{}, 1),
459 }
460
461 // Serve HTTP in goroutine
@@ -468,8 +470,15 @@ func ServeHTTPAsyncWithStreaming(handler http.Handler, jsReq js.Value) js.Value
470 }()
471
472 handler.ServeHTTP(respWriter, httpReq)
473 +
474 + if !respWriter.wroteHeader {
475 + respWriter.WriteHeader(http.StatusBadGateway)
476 + http.Error(respWriter, "Bad Gateway\n\nUpstream server error", http.StatusBadGateway)
477 + }
478 }()
479
480 + <-respWriter.wroteHeaderChan
481 +
482 // Create http.Response with streaming body
483 httpResp := &http.Response{
484 StatusCode: respWriter.statusCode,
@@ -489,10 +498,11 @@ func ServeHTTPAsyncWithStreaming(handler http.Handler, jsReq js.Value) js.Value
498
499 // streamingResponseWriter implements http.ResponseWriter for streaming responses
500 type streamingResponseWriter struct {
492 - pipeWriter *io.PipeWriter
493 - header http.Header
494 - statusCode int
495 - wroteHeader bool
501 + pipeWriter *io.PipeWriter
502 + header http.Header
503 + statusCode int
504 + wroteHeader bool
505 + wroteHeaderChan chan struct{}
506 }
507
508 func (w *streamingResponseWriter) Header() http.Header {
@@ -510,5 +520,6 @@ func (w *streamingResponseWriter) WriteHeader(statusCode int) {
520 if !w.wroteHeader {
521 w.statusCode = statusCode
522 w.wroteHeader = true
523 + close(w.wroteHeaderChan)
524 }
525 }
portal/core/cryptoops/sig.go
+2 -1
@@ -15,7 +15,8 @@ var _base32_encoding = base32.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567").Wi
15 func DeriveID(publickey ed25519.PublicKey) string {
16 h := hmac.New(sha256.New, _id_magic)
17 h.Write(publickey)
18 - return _base32_encoding.EncodeToString(h.Sum(nil))
18 + hash := h.Sum(nil)
19 + return _base32_encoding.EncodeToString(hash[:16])
20 }
21
22 type Credential struct {