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
+ })
+}