mirror of
https://github.com/minio/minio.git
synced 2025-07-20 22:11:13 -04:00
fix: lambda handler response to match the lambda return status (#21436)
This commit is contained in:
parent
de234b888c
commit
4021d8c8e2
@ -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"
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
174
cmd/object-lambda-handlers_test.go
Normal file
174
cmd/object-lambda-handlers_test.go
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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"},
|
||||
})
|
||||
}
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by "stringer -type=TargetType -trimprefix=Target types.go"; DO NOT EDIT.
|
||||
|
||||
package types
|
||||
package loggertypes
|
||||
|
||||
import "strconv"
|
||||
|
@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
package loggertypes
|
||||
|
||||
// TargetType indicates type of the target e.g. console, http, kafka
|
||||
type TargetType uint8
|
@ -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 (
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user