From 73a656b919a96b7a5f4f8b63bcbd34b733526bb2 Mon Sep 17 00:00:00 2001 From: ex-takashima Date: Thu, 12 Mar 2026 16:39:11 +0900 Subject: [PATCH] refactor(line): hoist body size const, add boundary tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move maxWebhookBodySize to package-level const - Add TestWebhookAcceptsMaxBodySize (exact limit → 403, not 413) - Add TestWebhookRejectsOversizedBodyBeforeSignatureCheck - Use const in test instead of magic number Co-Authored-By: Claude Opus 4.6 --- pkg/channels/line/line.go | 7 ++++--- pkg/channels/line/line_test.go | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/pkg/channels/line/line.go b/pkg/channels/line/line.go index c60c0ee1c..56ba02183 100644 --- a/pkg/channels/line/line.go +++ b/pkg/channels/line/line.go @@ -32,6 +32,10 @@ const ( lineBotInfoEndpoint = lineAPIBase + "/info" lineLoadingEndpoint = lineAPIBase + "/chat/loading/start" lineReplyTokenMaxAge = 25 * time.Second + + // Limit request body to prevent memory exhaustion (DoS). + // LINE webhook payloads are typically a few KB; 1 MiB is generous. + maxWebhookBodySize = 1 << 20 // 1 MiB ) type replyTokenEntry struct { @@ -166,9 +170,6 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) { return } - // Limit request body to prevent memory exhaustion (DoS). - // LINE webhook payloads are typically a few KB; 1 MB is generous. - const maxWebhookBodySize = 1 << 20 // 1 MB body, err := io.ReadAll(io.LimitReader(r.Body, maxWebhookBodySize+1)) if err != nil { logger.ErrorCF("line", "Failed to read request body", map[string]any{ diff --git a/pkg/channels/line/line_test.go b/pkg/channels/line/line_test.go index b8052c471..00770f1c7 100644 --- a/pkg/channels/line/line_test.go +++ b/pkg/channels/line/line_test.go @@ -11,8 +11,7 @@ import ( func TestWebhookRejectsOversizedBody(t *testing.T) { ch := &LINEChannel{} - // Create a body larger than maxWebhookBodySize (1 MB) - oversized := bytes.Repeat([]byte("A"), (1<<20)+1) + oversized := bytes.Repeat([]byte("A"), maxWebhookBodySize+1) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized)) rec := httptest.NewRecorder() @@ -23,6 +22,36 @@ func TestWebhookRejectsOversizedBody(t *testing.T) { } } +func TestWebhookAcceptsMaxBodySize(t *testing.T) { + ch := &LINEChannel{} + + body := bytes.Repeat([]byte("A"), maxWebhookBodySize) + req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) + rec := httptest.NewRecorder() + + ch.webhookHandler(rec, req) + + // Missing signature should be rejected, but the body size should not trigger 413. + if rec.Code != http.StatusForbidden { + t.Errorf("expected status %d, got %d", http.StatusForbidden, rec.Code) + } +} + +func TestWebhookRejectsOversizedBodyBeforeSignatureCheck(t *testing.T) { + ch := &LINEChannel{} + + oversized := bytes.Repeat([]byte("A"), maxWebhookBodySize+1) + req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized)) + req.Header.Set("X-Line-Signature", "invalidsignature") + rec := httptest.NewRecorder() + + ch.webhookHandler(rec, req) + + if rec.Code != http.StatusRequestEntityTooLarge { + t.Errorf("expected status %d, got %d", http.StatusRequestEntityTooLarge, rec.Code) + } +} + func TestWebhookRejectsNonPostMethod(t *testing.T) { ch := &LINEChannel{}