This is an automated email from the ASF dual-hosted git repository.

AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git


The following commit(s) were added to refs/heads/develop by this push:
     new c53ffa53 fix(hotreload): bound reload request body size (#977)
c53ffa53 is described below

commit c53ffa53e707857504184b7bb83af889232ca8fe
Author: aias00 <[email protected]>
AuthorDate: Fri Jun 12 09:43:53 2026 +0800

    fix(hotreload): bound reload request body size (#977)
    
    * Bound hot reload request bodies
    
    The reload endpoint accepted authenticated request bodies of arbitrary size 
and read them fully into memory. Limit the body before parsing YAML and reject 
oversized requests with 413 while keeping the existing empty-body file reload 
behavior.
    
    Constraint: Preserve existing authenticated reload behavior for normal and 
empty request bodies
    
    Rejected: Streaming YAML decode directly | larger behavior change than 
needed for this endpoint
    
    Confidence: high
    
    Scope-risk: narrow
    
    Directive: Keep reload body limits explicit when adding new reload input 
modes
    
    Tested: go test ./pkg/hotreload
    
    * Address hot reload review comments
    
    Use http.MaxBytesReader for reload body limits and add tests for exact-size 
bodies and empty-body fallback behavior. This keeps the existing file reload 
fallback while locking the request body boundary semantics.
    
    Constraint: Preserve repository imports-formatter grouping that CI enforces
    
    Rejected: Keep io.LimitReader | MaxBytesReader is the standard net/http 
request limit mechanism
    
    Confidence: high
    
    Scope-risk: narrow
    
    Tested: go test ./pkg/hotreload
---
 pkg/hotreload/http_handler.go      | 11 ++++-
 pkg/hotreload/http_handler_test.go | 85 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 95 insertions(+), 1 deletion(-)

diff --git a/pkg/hotreload/http_handler.go b/pkg/hotreload/http_handler.go
index 737f66a9..1cb3df20 100644
--- a/pkg/hotreload/http_handler.go
+++ b/pkg/hotreload/http_handler.go
@@ -19,6 +19,7 @@ package hotreload
 
 import (
        "context"
+       "crypto/subtle"
        "encoding/json"
        "fmt"
        "io"
@@ -39,6 +40,8 @@ import (
        "github.com/apache/dubbo-go-pixiu/pkg/model"
 )
 
+const maxReloadBodyBytes = 1 << 20 // 1 MiB
+
 var (
        reloadMutex    sync.Mutex
        configPath     string
@@ -69,7 +72,7 @@ func checkAuth(r *http.Request) bool {
        // Check shared secret if configured
        if reloadSecret != "" {
                token := r.Header.Get("X-Reload-Token")
-               return token == reloadSecret
+               return subtle.ConstantTimeCompare([]byte(token), 
[]byte(reloadSecret)) == 1
        }
 
        // If no secret configured and not localhost, deny
@@ -95,9 +98,15 @@ func (h *ReloadHandler) ServeHTTP(w http.ResponseWriter, r 
*http.Request) {
        // Try to read from body first (handles chunked encoding where 
ContentLength == -1)
        // If body is empty, fallback to file reload
        if r.Body != nil {
+               r.Body = http.MaxBytesReader(w, r.Body, maxReloadBodyBytes)
                content, readErr := io.ReadAll(r.Body)
 
                if readErr != nil {
+                       if _, ok := readErr.(*http.MaxBytesError); ok {
+                               logger.Warnf("Reload request body from %s 
exceeded %d bytes", r.RemoteAddr, maxReloadBodyBytes)
+                               http.Error(w, "Request body too large", 
http.StatusRequestEntityTooLarge)
+                               return
+                       }
                        logger.Errorf("Failed to read request body: %v", 
readErr)
                        http.Error(w, fmt.Sprintf("Failed to read request body: 
%v", readErr), http.StatusBadRequest)
                        return
diff --git a/pkg/hotreload/http_handler_test.go 
b/pkg/hotreload/http_handler_test.go
new file mode 100644
index 00000000..5ee8c519
--- /dev/null
+++ b/pkg/hotreload/http_handler_test.go
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package hotreload
+
+import (
+       "net/http"
+       "net/http/httptest"
+       "strings"
+       "testing"
+)
+
+import (
+       "github.com/stretchr/testify/assert"
+)
+
+func TestReloadHandlerRejectsOversizedBody(t *testing.T) {
+       withReloadSecret(t, "test-secret")
+
+       req := httptest.NewRequest(http.MethodPost, "/-/reload", 
strings.NewReader(strings.Repeat("a", maxReloadBodyBytes+1)))
+       req.RemoteAddr = "192.0.2.1:12345"
+       req.Header.Set("X-Reload-Token", "test-secret")
+
+       rr := httptest.NewRecorder()
+       (&ReloadHandler{}).ServeHTTP(rr, req)
+
+       assert.Equal(t, http.StatusRequestEntityTooLarge, rr.Code)
+}
+
+func TestReloadHandlerAllowsBodyAtSizeLimit(t *testing.T) {
+       withReloadSecret(t, "test-secret")
+
+       req := httptest.NewRequest(http.MethodPost, "/-/reload", 
strings.NewReader(strings.Repeat("a", maxReloadBodyBytes)))
+       req.RemoteAddr = "192.0.2.1:12345"
+       req.Header.Set("X-Reload-Token", "test-secret")
+
+       rr := httptest.NewRecorder()
+       (&ReloadHandler{}).ServeHTTP(rr, req)
+
+       assert.NotEqual(t, http.StatusRequestEntityTooLarge, rr.Code)
+       assert.Equal(t, http.StatusInternalServerError, rr.Code)
+}
+
+func TestReloadHandlerEmptyBodyFallsBackToFileReload(t *testing.T) {
+       withReloadSecret(t, "test-secret")
+       oldConfigPath := configPath
+       configPath = ""
+       t.Cleanup(func() {
+               configPath = oldConfigPath
+       })
+
+       req := httptest.NewRequest(http.MethodPost, "/-/reload", 
strings.NewReader(""))
+       req.RemoteAddr = "192.0.2.1:12345"
+       req.Header.Set("X-Reload-Token", "test-secret")
+
+       rr := httptest.NewRecorder()
+       (&ReloadHandler{}).ServeHTTP(rr, req)
+
+       assert.Equal(t, http.StatusInternalServerError, rr.Code)
+       assert.Contains(t, rr.Body.String(), "config path not set")
+}
+
+func withReloadSecret(t *testing.T, secret string) {
+       t.Helper()
+
+       oldSecret := reloadSecret
+       reloadSecret = secret
+       t.Cleanup(func() {
+               reloadSecret = oldSecret
+       })
+}

Reply via email to