From 57a8951281aa77e23453186248ae6c3db6fd0846 Mon Sep 17 00:00:00 2001 From: Eider Oliveira Date: Fri, 8 May 2026 14:20:14 -0300 Subject: [PATCH] fix: cross-origin fetch noise, bytes.Reader race, and form decoding fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchInterceptor: skip cross-origin requests (e.g. Google Analytics) entirely — we have no insight into them and forcing a JSON parse on their bodies just produces console noise. Also swallow JSON parse errors on same-origin responses: a non-JSON body is not an error, only the request lifecycle matters. builder: create a new bytes.Reader per request in PacksHandler to avoid a data race when concurrent requests share the same reader (its seek offset is mutated by http.ServeContent). api: fall back to r.Form when MultipartForm is nil so that URL-encoded POST bodies are decoded correctly. --- api.go | 11 ++++-- builder.go | 4 +- corejs/src/fetchInterceptor.ts | 71 ++++++++++++++++++++++------------ 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/api.go b/api.go index e25c627..1998438 100644 --- a/api.go +++ b/api.go @@ -122,18 +122,23 @@ func (ctx *EventContext) MustUnmarshalForm(v interface{}) { func (ctx *EventContext) UnmarshalForm(v interface{}) (err error) { mf := ctx.R.MultipartForm - if ctx.R.MultipartForm == nil { + var values map[string][]string + if mf != nil { + values = mf.Value + } else if ctx.R.Form != nil { + values = ctx.R.Form + } else { return } dec := form.NewDecoder() - err = dec.Decode(v, mf.Value) + err = dec.Decode(v, values) if err != nil { // panic(err) return } - if len(mf.File) > 0 { + if mf != nil && len(mf.File) > 0 { for k, vs := range mf.File { _ = reflectutils.Set(v, k, vs) } diff --git a/builder.go b/builder.go index c516f84..c20e145 100644 --- a/builder.go +++ b/builder.go @@ -55,11 +55,11 @@ func (b *Builder) PacksHandler(contentType string, packs ...ComponentsPack) http buf.WriteString("\n\n") } - body := bytes.NewReader(buf.Bytes()) + bodyBytes := buf.Bytes() return gziphandler.GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", contentType) - http.ServeContent(w, r, "", startTime, body) + http.ServeContent(w, r, "", startTime, bytes.NewReader(bodyBytes)) })) } diff --git a/corejs/src/fetchInterceptor.ts b/corejs/src/fetchInterceptor.ts index 30791ca..b50784e 100644 --- a/corejs/src/fetchInterceptor.ts +++ b/corejs/src/fetchInterceptor.ts @@ -12,6 +12,39 @@ const requestMap = new Map { const [resource, config] = args + // Skip cross-origin requests (e.g. Google Analytics): we have no insight + // into them and forcing a JSON parse on their bodies just produces noise. + if (isCrossOrigin(resource)) { + return originalFetch(...args) + } + // Generate a unique ID for the request const requestId = generateUniqueId() @@ -39,31 +78,13 @@ export function initFetchInterceptor(customInterceptor: FetchInterceptor) { // Clone the response to preserve the original response for further use const clonedResponse = response.clone() - // Start processing the response body without waiting - const processingPromise = clonedResponse.json() - - processingPromise - .then(() => { - const requestInfo = requestMap.get(requestId) - - if (customInterceptor.onResponse && requestInfo) { - const resource = - requestInfo.resource instanceof URL - ? requestInfo.resource.toString() - : requestInfo.resource - - customInterceptor.onResponse( - requestId, - response, // Pass the original response - resource, - requestInfo.config - ) - } - - requestMap.delete(requestId) - }) - .catch((error: unknown) => { - errorHandler(error, requestId, customInterceptor) + // Start processing the response body without waiting. A non-JSON or + // empty body is not an error — only the request lifecycle matters here. + clonedResponse + .json() + .catch(() => undefined) + .finally(() => { + notifyResponse(requestId, response, customInterceptor) }) // Return the original response