diff --git a/cmd/consolelogger.go b/cmd/consolelogger.go
index 624e96855..676a30ca3 100644
--- a/cmd/consolelogger.go
+++ b/cmd/consolelogger.go
@@ -28,7 +28,7 @@ import (
"github.com/minio/madmin-go/v3/logger/log"
"github.com/minio/minio/internal/logger"
"github.com/minio/minio/internal/logger/target/console"
- "github.com/minio/minio/internal/logger/target/types"
+ types "github.com/minio/minio/internal/logger/target/loggertypes"
"github.com/minio/minio/internal/pubsub"
xnet "github.com/minio/pkg/v3/net"
)
diff --git a/cmd/object-lambda-handlers.go b/cmd/object-lambda-handlers.go
index c486e94bc..1ced5165d 100644
--- a/cmd/object-lambda-handlers.go
+++ b/cmd/object-lambda-handlers.go
@@ -23,6 +23,8 @@ import (
"io"
"net/http"
"net/url"
+ "strconv"
+ "strings"
"time"
"github.com/klauspost/compress/gzhttp"
@@ -39,7 +41,7 @@ import (
"github.com/minio/minio/internal/logger"
)
-func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Request) (levent.Event, error) {
+var getLambdaEventData = func(bucket, object string, cred auth.Credentials, r *http.Request) (levent.Event, error) {
host := globalLocalNodeName
secure := globalIsTLS
if globalMinioEndpointURL != nil {
@@ -100,80 +102,6 @@ func getLambdaEventData(bucket, object string, cred auth.Credentials, r *http.Re
return eventData, nil
}
-var statusTextToCode = map[string]int{
- "Continue": http.StatusContinue,
- "Switching Protocols": http.StatusSwitchingProtocols,
- "Processing": http.StatusProcessing,
- "Early Hints": http.StatusEarlyHints,
- "OK": http.StatusOK,
- "Created": http.StatusCreated,
- "Accepted": http.StatusAccepted,
- "Non-Authoritative Information": http.StatusNonAuthoritativeInfo,
- "No Content": http.StatusNoContent,
- "Reset Content": http.StatusResetContent,
- "Partial Content": http.StatusPartialContent,
- "Multi-Status": http.StatusMultiStatus,
- "Already Reported": http.StatusAlreadyReported,
- "IM Used": http.StatusIMUsed,
- "Multiple Choices": http.StatusMultipleChoices,
- "Moved Permanently": http.StatusMovedPermanently,
- "Found": http.StatusFound,
- "See Other": http.StatusSeeOther,
- "Not Modified": http.StatusNotModified,
- "Use Proxy": http.StatusUseProxy,
- "Temporary Redirect": http.StatusTemporaryRedirect,
- "Permanent Redirect": http.StatusPermanentRedirect,
- "Bad Request": http.StatusBadRequest,
- "Unauthorized": http.StatusUnauthorized,
- "Payment Required": http.StatusPaymentRequired,
- "Forbidden": http.StatusForbidden,
- "Not Found": http.StatusNotFound,
- "Method Not Allowed": http.StatusMethodNotAllowed,
- "Not Acceptable": http.StatusNotAcceptable,
- "Proxy Authentication Required": http.StatusProxyAuthRequired,
- "Request Timeout": http.StatusRequestTimeout,
- "Conflict": http.StatusConflict,
- "Gone": http.StatusGone,
- "Length Required": http.StatusLengthRequired,
- "Precondition Failed": http.StatusPreconditionFailed,
- "Request Entity Too Large": http.StatusRequestEntityTooLarge,
- "Request URI Too Long": http.StatusRequestURITooLong,
- "Unsupported Media Type": http.StatusUnsupportedMediaType,
- "Requested Range Not Satisfiable": http.StatusRequestedRangeNotSatisfiable,
- "Expectation Failed": http.StatusExpectationFailed,
- "I'm a teapot": http.StatusTeapot,
- "Misdirected Request": http.StatusMisdirectedRequest,
- "Unprocessable Entity": http.StatusUnprocessableEntity,
- "Locked": http.StatusLocked,
- "Failed Dependency": http.StatusFailedDependency,
- "Too Early": http.StatusTooEarly,
- "Upgrade Required": http.StatusUpgradeRequired,
- "Precondition Required": http.StatusPreconditionRequired,
- "Too Many Requests": http.StatusTooManyRequests,
- "Request Header Fields Too Large": http.StatusRequestHeaderFieldsTooLarge,
- "Unavailable For Legal Reasons": http.StatusUnavailableForLegalReasons,
- "Internal Server Error": http.StatusInternalServerError,
- "Not Implemented": http.StatusNotImplemented,
- "Bad Gateway": http.StatusBadGateway,
- "Service Unavailable": http.StatusServiceUnavailable,
- "Gateway Timeout": http.StatusGatewayTimeout,
- "HTTP Version Not Supported": http.StatusHTTPVersionNotSupported,
- "Variant Also Negotiates": http.StatusVariantAlsoNegotiates,
- "Insufficient Storage": http.StatusInsufficientStorage,
- "Loop Detected": http.StatusLoopDetected,
- "Not Extended": http.StatusNotExtended,
- "Network Authentication Required": http.StatusNetworkAuthenticationRequired,
-}
-
-// StatusCode returns a HTTP Status code for the HTTP text. It returns -1
-// if the text is unknown.
-func StatusCode(text string) int {
- if code, ok := statusTextToCode[text]; ok {
- return code
- }
- return -1
-}
-
func fwdHeadersToS3(h http.Header, w http.ResponseWriter) {
const trim = "x-amz-fwd-header-"
for k, v := range h {
@@ -183,19 +111,26 @@ func fwdHeadersToS3(h http.Header, w http.ResponseWriter) {
}
}
-func fwdStatusToAPIError(resp *http.Response) *APIError {
- if status := resp.Header.Get(xhttp.AmzFwdStatus); status != "" && StatusCode(status) > -1 {
- apiErr := &APIError{
- HTTPStatusCode: StatusCode(status),
- Description: resp.Header.Get(xhttp.AmzFwdErrorMessage),
- Code: resp.Header.Get(xhttp.AmzFwdErrorCode),
- }
- if apiErr.HTTPStatusCode == http.StatusOK {
- return nil
- }
- return apiErr
+func fwdStatusToAPIError(statusCode int, resp *http.Response) *APIError {
+ if statusCode < http.StatusBadRequest {
+ return nil
+ }
+ desc := resp.Header.Get(xhttp.AmzFwdErrorMessage)
+ if strings.TrimSpace(desc) == "" {
+ apiErr := errorCodes.ToAPIErr(ErrInvalidRequest)
+ return &apiErr
+ }
+ code := resp.Header.Get(xhttp.AmzFwdErrorCode)
+ if strings.TrimSpace(code) == "" {
+ apiErr := errorCodes.ToAPIErr(ErrInvalidRequest)
+ apiErr.Description = desc
+ return &apiErr
+ }
+ return &APIError{
+ HTTPStatusCode: statusCode,
+ Description: desc,
+ Code: code,
}
- return nil
}
// GetObjectLambdaHandler - GET Object with transformed data via lambda functions
@@ -262,26 +197,31 @@ func (api objectAPIHandlers) GetObjectLambdaHandler(w http.ResponseWriter, r *ht
return
}
+ statusCode := resp.StatusCode
+ if status := resp.Header.Get(xhttp.AmzFwdStatus); status != "" {
+ statusCode, err = strconv.Atoi(status)
+ if err != nil {
+ writeErrorResponse(ctx, w, APIError{
+ Code: "LambdaFunctionStatusError",
+ HTTPStatusCode: http.StatusBadRequest,
+ Description: err.Error(),
+ }, r.URL)
+ return
+ }
+ }
+
// Set all the relevant lambda forward headers if found.
fwdHeadersToS3(resp.Header, w)
- if apiErr := fwdStatusToAPIError(resp); apiErr != nil {
+ if apiErr := fwdStatusToAPIError(statusCode, resp); apiErr != nil {
writeErrorResponse(ctx, w, *apiErr, r.URL)
return
}
- if resp.StatusCode != http.StatusOK {
- writeErrorResponse(ctx, w, APIError{
- Code: "LambdaFunctionError",
- HTTPStatusCode: resp.StatusCode,
- Description: "unexpected failure reported from lambda function",
- }, r.URL)
- return
- }
-
if !globalAPIConfig.shouldGzipObjects() {
w.Header().Set(gzhttp.HeaderNoCompression, "true")
}
+ w.WriteHeader(statusCode)
io.Copy(w, resp.Body)
}
diff --git a/cmd/object-lambda-handlers_test.go b/cmd/object-lambda-handlers_test.go
new file mode 100644
index 000000000..458034fec
--- /dev/null
+++ b/cmd/object-lambda-handlers_test.go
@@ -0,0 +1,174 @@
+// Copyright (c) 2015-2025 MinIO, Inc.
+//
+// This file is part of MinIO Object Storage stack
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package cmd
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/minio/minio-go/v7/pkg/signer"
+ "github.com/minio/minio/internal/auth"
+ "github.com/minio/minio/internal/config"
+ "github.com/minio/minio/internal/config/lambda"
+ levent "github.com/minio/minio/internal/config/lambda/event"
+ xhttp "github.com/minio/minio/internal/http"
+)
+
+func TestGetObjectLambdaHandler(t *testing.T) {
+ testCases := []struct {
+ name string
+ statusCode int
+ body string
+ contentType string
+ expectStatus int
+ }{
+ {
+ name: "Success 206 Partial Content",
+ statusCode: 206,
+ body: "partial-object-data",
+ contentType: "text/plain",
+ expectStatus: 206,
+ },
+ {
+ name: "Success 200 OK",
+ statusCode: 200,
+ body: "full-object-data",
+ contentType: "application/json",
+ expectStatus: 200,
+ },
+ {
+ name: "Client Error 400",
+ statusCode: 400,
+ body: "bad-request",
+ contentType: "application/xml",
+ expectStatus: 400,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ runObjectLambdaTest(t, tc.statusCode, tc.body, tc.contentType, tc.expectStatus)
+ })
+ }
+}
+
+func runObjectLambdaTest(t *testing.T, lambdaStatus int, lambdaBody, contentType string, expectStatus int) {
+ ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{
+ t: t,
+ objAPITest: func(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, credentials auth.Credentials, t *testing.T) {
+ objectName := "dummy-object"
+ functionID := "lambda1"
+ functionToken := "token123"
+
+ // Lambda mock server
+ lambdaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set(xhttp.AmzRequestRoute, functionID)
+ w.Header().Set(xhttp.AmzRequestToken, functionToken)
+ w.Header().Set(xhttp.AmzFwdHeaderContentType, contentType)
+ w.Header().Set(xhttp.AmzFwdStatus, strconv.Itoa(lambdaStatus))
+ w.WriteHeader(lambdaStatus)
+ w.Write([]byte(lambdaBody))
+ }))
+ defer lambdaServer.Close()
+
+ lambdaARN := "arn:minio:s3-object-lambda::lambda1:webhook"
+
+ cfg := config.New()
+ cfg[config.LambdaWebhookSubSys] = map[string]config.KVS{
+ functionID: {
+ {Key: "endpoint", Value: lambdaServer.URL},
+ {Key: "enable", Value: config.EnableOn},
+ },
+ }
+ cfg[config.APISubSys] = map[string]config.KVS{
+ "api": {
+ {Key: "gzip", Value: config.EnableOff},
+ },
+ }
+
+ var err error
+ globalLambdaTargetList, err = lambda.FetchEnabledTargets(context.Background(), cfg, http.DefaultTransport.(*http.Transport))
+ if err != nil {
+ t.Fatalf("failed to load lambda targets: %v", err)
+ }
+
+ getLambdaEventData = func(_, _ string, _ auth.Credentials, _ *http.Request) (levent.Event, error) {
+ return levent.Event{
+ GetObjectContext: &levent.GetObjectContext{
+ OutputRoute: functionID,
+ OutputToken: functionToken,
+ InputS3URL: "http://localhost/dummy",
+ },
+ UserRequest: levent.UserRequest{
+ Headers: map[string][]string{},
+ },
+ UserIdentity: levent.Identity{
+ PrincipalID: "test-user",
+ },
+ }, nil
+ }
+
+ body := []byte{}
+ req := httptest.NewRequest("GET", "/objectlambda/"+bucketName+"/"+objectName+"?lambdaArn="+url.QueryEscape(lambdaARN), bytes.NewReader(body))
+ req.Form = url.Values{"lambdaArn": []string{lambdaARN}}
+ req.Header.Set("Host", "localhost")
+ req.Header.Set("X-Amz-Date", time.Now().UTC().Format("20060102T150405Z"))
+ sum := sha256.Sum256(body)
+ req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum[:]))
+ req = signer.SignV4(*req, credentials.AccessKey, credentials.SecretKey, "", "us-east-1")
+
+ rec := httptest.NewRecorder()
+ api := objectAPIHandlers{
+ ObjectAPI: func() ObjectLayer {
+ return obj
+ },
+ }
+ api.GetObjectLambdaHandler(rec, req)
+
+ res := rec.Result()
+ defer res.Body.Close()
+ respBody, _ := io.ReadAll(res.Body)
+
+ if res.StatusCode != expectStatus {
+ t.Errorf("Expected status %d, got %d", expectStatus, res.StatusCode)
+ }
+
+ if contentType != "" {
+ if ct := res.Header.Get("Content-Type"); ct != contentType {
+ t.Errorf("Expected Content-Type %q, got %q", contentType, ct)
+ }
+ }
+
+ if res.StatusCode < 400 {
+ if string(respBody) != lambdaBody {
+ t.Errorf("Expected body %q, got %q", lambdaBody, string(respBody))
+ }
+ }
+ },
+ endpoints: []string{"GetObject"},
+ })
+}
diff --git a/internal/logger/target/http/http.go b/internal/logger/target/http/http.go
index 55c933e40..a877cf162 100644
--- a/internal/logger/target/http/http.go
+++ b/internal/logger/target/http/http.go
@@ -35,7 +35,7 @@ import (
jsoniter "github.com/json-iterator/go"
xhttp "github.com/minio/minio/internal/http"
xioutil "github.com/minio/minio/internal/ioutil"
- "github.com/minio/minio/internal/logger/target/types"
+ types "github.com/minio/minio/internal/logger/target/loggertypes"
"github.com/minio/minio/internal/once"
"github.com/minio/minio/internal/store"
xnet "github.com/minio/pkg/v3/net"
diff --git a/internal/logger/target/kafka/kafka.go b/internal/logger/target/kafka/kafka.go
index a6034adf4..2fb488914 100644
--- a/internal/logger/target/kafka/kafka.go
+++ b/internal/logger/target/kafka/kafka.go
@@ -35,7 +35,7 @@ import (
saramatls "github.com/IBM/sarama/tools/tls"
xioutil "github.com/minio/minio/internal/ioutil"
- "github.com/minio/minio/internal/logger/target/types"
+ types "github.com/minio/minio/internal/logger/target/loggertypes"
"github.com/minio/minio/internal/once"
"github.com/minio/minio/internal/store"
xnet "github.com/minio/pkg/v3/net"
diff --git a/internal/logger/target/types/targettype_string.go b/internal/logger/target/loggertypes/targettype_string.go
similarity index 97%
rename from internal/logger/target/types/targettype_string.go
rename to internal/logger/target/loggertypes/targettype_string.go
index 6aa5e3974..715a9fef1 100644
--- a/internal/logger/target/types/targettype_string.go
+++ b/internal/logger/target/loggertypes/targettype_string.go
@@ -1,6 +1,6 @@
// Code generated by "stringer -type=TargetType -trimprefix=Target types.go"; DO NOT EDIT.
-package types
+package loggertypes
import "strconv"
diff --git a/internal/logger/target/types/types.go b/internal/logger/target/loggertypes/types.go
similarity index 98%
rename from internal/logger/target/types/types.go
rename to internal/logger/target/loggertypes/types.go
index d20b0cc02..8d8e71041 100644
--- a/internal/logger/target/types/types.go
+++ b/internal/logger/target/loggertypes/types.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package types
+package loggertypes
// TargetType indicates type of the target e.g. console, http, kafka
type TargetType uint8
diff --git a/internal/logger/target/testlogger/testlogger.go b/internal/logger/target/testlogger/testlogger.go
index 52cff89e7..d2b4149a0 100644
--- a/internal/logger/target/testlogger/testlogger.go
+++ b/internal/logger/target/testlogger/testlogger.go
@@ -36,7 +36,7 @@ import (
"github.com/minio/madmin-go/v3/logger/log"
"github.com/minio/minio/internal/logger"
- "github.com/minio/minio/internal/logger/target/types"
+ types "github.com/minio/minio/internal/logger/target/loggertypes"
)
const (
diff --git a/internal/logger/targets.go b/internal/logger/targets.go
index 26f0c7b97..b4df2e5c8 100644
--- a/internal/logger/targets.go
+++ b/internal/logger/targets.go
@@ -25,7 +25,7 @@ import (
"github.com/minio/minio/internal/logger/target/http"
"github.com/minio/minio/internal/logger/target/kafka"
- "github.com/minio/minio/internal/logger/target/types"
+ types "github.com/minio/minio/internal/logger/target/loggertypes"
)
// Target is the entity that we will receive