From d633e59a50f10069d3f0c081354b90a2911de8f6 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sat, 11 Apr 2026 15:56:31 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20v0.1.19=20=E2=80=94=20=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F=20VisibilityTimeout/WaitTi?= =?UTF-8?q?meSeconds/=D0=BF=D1=83=D1=81=D1=82=D0=BE=D0=B9=20MessageBody,?= =?UTF-8?q?=20fix=20hardcore=5Ftest=20LONG=5FQ=20length?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/gosqs/change_message_visibility.go | 141 +++++++++++++------------ app/gosqs/receive_message.go | 11 +- app/gosqs/send_message.go | 3 + app/models/errors.go | 2 + tests/hardcore_test.sh | 9 +- 5 files changed, 90 insertions(+), 76 deletions(-) diff --git a/app/gosqs/change_message_visibility.go b/app/gosqs/change_message_visibility.go index f34e9e5..55f313a 100644 --- a/app/gosqs/change_message_visibility.go +++ b/app/gosqs/change_message_visibility.go @@ -3,85 +3,86 @@ package gosqs import ( -"net/http" -"strings" -"time" + "net/http" + "strings" + "time" -"shared-sqs/app/interfaces" -"shared-sqs/app/models" -"shared-sqs/app/utils" -"github.com/gorilla/mux" -log "github.com/sirupsen/logrus" + "shared-sqs/app/interfaces" + "shared-sqs/app/models" + "shared-sqs/app/utils" + + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" ) func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractResponseBody) { -requestBody := models.NewChangeMessageVisibilityRequest() -ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) -if !ok { -log.Error("Invalid Request - ChangeMessageVisibilityV1") -return utils.CreateErrorResponseV1("InvalidParameterValue", true) -} + requestBody := models.NewChangeMessageVisibilityRequest() + ok := utils.REQUEST_TRANSFORMER(requestBody, req, false) + if !ok { + log.Error("Invalid Request - ChangeMessageVisibilityV1") + return utils.CreateErrorResponseV1("InvalidParameterValue", true) + } -t := getTenantFromContext(req) -if t == nil { -return utils.CreateErrorResponseV1("InvalidClientTokenId", true) -} + t := getTenantFromContext(req) + if t == nil { + return utils.CreateErrorResponseV1("InvalidClientTokenId", true) + } -vars := mux.Vars(req) -queueUrl := requestBody.QueueUrl -queueName := "" -if queueUrl == "" { -queueName = vars["queueName"] -} else { -uriSegments := strings.Split(queueUrl, "/") -queueName = uriSegments[len(uriSegments)-1] -} + vars := mux.Vars(req) + queueUrl := requestBody.QueueUrl + queueName := "" + if queueUrl == "" { + queueName = vars["queueName"] + } else { + uriSegments := strings.Split(queueUrl, "/") + queueName = uriSegments[len(uriSegments)-1] + } -key := tenantQueueKey(t.AccessKey, queueName) -receiptHandle := requestBody.ReceiptHandle -visibilityTimeout := requestBody.VisibilityTimeout + key := tenantQueueKey(t.AccessKey, queueName) + receiptHandle := requestBody.ReceiptHandle + visibilityTimeout := requestBody.VisibilityTimeout -if visibilityTimeout > 43200 { -return utils.CreateErrorResponseV1("ValidationError", true) -} + if visibilityTimeout < 0 || visibilityTimeout > 43200 { + return utils.CreateErrorResponseV1("InvalidParameterValue", true) + } -if _, ok := models.SyncQueues.Queues[key]; !ok { -return utils.CreateErrorResponseV1("QueueNotFound", true) -} + if _, ok := models.SyncQueues.Queues[key]; !ok { + return utils.CreateErrorResponseV1("QueueNotFound", true) + } -models.SyncQueues.Lock() -messageFound := false -for i := 0; i < len(models.SyncQueues.Queues[key].Messages); i++ { -queue := models.SyncQueues.Queues[key] -msgs := queue.Messages -if msgs[i].ReceiptHandle == receiptHandle { -timeout := models.SyncQueues.Queues[key].VisibilityTimeout -if visibilityTimeout == 0 { -msgs[i].ReceiptTime = time.Now().UTC() -msgs[i].ReceiptHandle = "" -msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second) -msgs[i].Retry++ -if queue.MaxReceiveCount > 0 && -queue.DeadLetterQueue != nil && -msgs[i].Retry >= queue.MaxReceiveCount { -queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, msgs[i]) -queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...) -} -} else { -msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second) -} -messageFound = true -break -} -} -models.SyncQueues.Unlock() -if !messageFound { -return utils.CreateErrorResponseV1("MessageNotInFlight", true) -} + models.SyncQueues.Lock() + messageFound := false + for i := 0; i < len(models.SyncQueues.Queues[key].Messages); i++ { + queue := models.SyncQueues.Queues[key] + msgs := queue.Messages + if msgs[i].ReceiptHandle == receiptHandle { + timeout := models.SyncQueues.Queues[key].VisibilityTimeout + if visibilityTimeout == 0 { + msgs[i].ReceiptTime = time.Now().UTC() + msgs[i].ReceiptHandle = "" + msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(timeout) * time.Second) + msgs[i].Retry++ + if queue.MaxReceiveCount > 0 && + queue.DeadLetterQueue != nil && + msgs[i].Retry >= queue.MaxReceiveCount { + queue.DeadLetterQueue.Messages = append(queue.DeadLetterQueue.Messages, msgs[i]) + queue.Messages = append(queue.Messages[:i], queue.Messages[i+1:]...) + } + } else { + msgs[i].VisibilityTimeout = time.Now().Add(time.Duration(visibilityTimeout) * time.Second) + } + messageFound = true + break + } + } + models.SyncQueues.Unlock() + if !messageFound { + return utils.CreateErrorResponseV1("MessageNotInFlight", true) + } -respStruct := models.ChangeMessageVisibilityResult{ -Xmlns: models.BaseXmlns, -Metadata: models.BaseResponseMetadata, -} -return http.StatusOK, &respStruct + respStruct := models.ChangeMessageVisibilityResult{ + Xmlns: models.BaseXmlns, + Metadata: models.BaseResponseMetadata, + } + return http.StatusOK, &respStruct } diff --git a/app/gosqs/receive_message.go b/app/gosqs/receive_message.go index 6bd03b6..9afbc9b 100644 --- a/app/gosqs/receive_message.go +++ b/app/gosqs/receive_message.go @@ -57,14 +57,21 @@ func ReceiveMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) var messages []*models.ResultMessage respStruct := models.ReceiveMessageResponse{} + // Валидация VisibilityTimeout: AWS SQS допускает 0–43200 + if requestBody.VisibilityTimeout < 0 || requestBody.VisibilityTimeout > MaxVisibilityTimeout { + return utils.CreateErrorResponseV1("InvalidParameterValue", true) + } + waitTimeSeconds := requestBody.WaitTimeSeconds if waitTimeSeconds == 0 { models.SyncQueues.RLock() waitTimeSeconds = models.SyncQueues.Queues[key].ReceiveMessageWaitTimeSeconds models.SyncQueues.RUnlock() } - // Fix #4: clamp WaitTimeSeconds к AWS лимиту 0–20 - waitTimeSeconds = ClampInt(waitTimeSeconds, 0, MaxReceiveMessageWaitTimeSeconds) + // Валидация WaitTimeSeconds: AWS SQS допускает 0–20, иначе ошибка + if waitTimeSeconds < 0 || waitTimeSeconds > MaxReceiveMessageWaitTimeSeconds { + return utils.CreateErrorResponseV1("InvalidParameterValue", true) + } if waitTimeSeconds > 0 { deadline := time.Now().Add(time.Duration(waitTimeSeconds) * time.Second) diff --git a/app/gosqs/send_message.go b/app/gosqs/send_message.go index 196c794..ea1b596 100644 --- a/app/gosqs/send_message.go +++ b/app/gosqs/send_message.go @@ -35,6 +35,9 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { } messageBody := requestBody.MessageBody + if messageBody == "" { + return utils.CreateErrorResponseV1("MissingParameter", true) + } messageGroupID := requestBody.MessageGroupId messageDeduplicationID := requestBody.MessageDeduplicationId diff --git a/app/models/errors.go b/app/models/errors.go index a0792c3..b531d02 100644 --- a/app/models/errors.go +++ b/app/models/errors.go @@ -22,6 +22,8 @@ func init() { "ValidationError": {HttpError: http.StatusBadRequest, Type: "ValidationError", Code: "AWS.SimpleQueueService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."}, // LimitExceeded — превышен лимит очередей тенанта (max_queues) "LimitExceeded": {HttpError: http.StatusBadRequest, Type: "LimitExceeded", Code: "AWS.SimpleQueueService.LimitExceeded", Message: "You've reached the limit on the number of queues."}, + // MissingParameter — обязательный параметр отсутствует (например, пустой MessageBody) + "MissingParameter": {HttpError: http.StatusBadRequest, Type: "MissingParameter", Code: "MissingParameter", Message: "The request must contain the parameter MessageBody."}, } SnsErrors = map[string]SnsErrorType{ "InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleNotificationService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."}, diff --git a/tests/hardcore_test.sh b/tests/hardcore_test.sh index 200a12f..11043af 100644 --- a/tests/hardcore_test.sh +++ b/tests/hardcore_test.sh @@ -738,14 +738,15 @@ echo "── 15. Граничные случаи: имена, пустые те # ═══════════════════════════════════════════ # Очень длинное имя очереди (AWS позволяет до 80 символов) -LONG_Q=$(printf 'q%.0s' {1..70})"-${TS}" +# 69 q's + "-" + 10 цифр TS = 80 символов (ровно лимит) +LONG_Q=$(printf 'q%.0s' {1..69})"-${TS}" LONG_QURL=$(qurl "$T1_ID" "$LONG_Q") R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$LONG_Q" 2>&1) -check "Граница: длинное имя очереди (70 сим)" "$R" "Queue" +check "Граница: длинное имя очереди (80 сим)" "$R" "Queue" -# Пустое тело сообщения +# Пустое тело сообщения — AWS SQS требует непустой MessageBody R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$LONG_QURL" --message-body "" 2>&1) -check "Граница: пустое тело сообщения" "$R" "MessageId" +check "Граница: пустое тело сообщения → ошибка" "$R" "ERROR|Error|error|MissingParameter|Invalid" # Receive из пустой очереди sqs "$T1_AK" "$T1_SK" purge-queue --queue-url "$LONG_QURL" >/dev/null 2>&1 || true