Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions service/vefaas/retry_header_signing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package vefaas_test

// Confirms the retry attempt headers (X-Sdk-Invocation-Id, X-Sdk-Request) are
// present before signing runs, so they end up covered by the request's
// signature (Authorization: ...SignedHeaders=...) instead of being appended
// after signing.

import (
"strings"
"testing"

"github.com/volcengine/volcengine-go-sdk/service/vefaas"
"github.com/volcengine/volcengine-go-sdk/volcengine"
"github.com/volcengine/volcengine-go-sdk/volcengine/credentials"
"github.com/volcengine/volcengine-go-sdk/volcengine/session"
)

func TestCreateSandboxRequestSignsRetryHeaders(t *testing.T) {
sess, err := session.NewSession(volcengine.NewConfig().
WithRegion("cn-beijing").
WithEndpoint("https://example.com").
WithCredentials(credentials.NewStaticCredentials("test-ak", "test-sk", "")))
if err != nil {
t.Fatalf("failed to create session: %v", err)
}

client := vefaas.New(sess)
req, _ := client.CreateSandboxRequest(&vefaas.CreateSandboxInput{
FunctionId: volcengine.String("fn-test"),
})

if err := req.Sign(); err != nil {
t.Fatalf("failed to sign request: %v", err)
}

if req.HTTPRequest.Header.Get("X-Sdk-Invocation-Id") == "" {
t.Fatal("X-Sdk-Invocation-Id header missing before signing")
}
if req.HTTPRequest.Header.Get("X-Sdk-Request") == "" {
t.Fatal("X-Sdk-Request header missing before signing")
}

auth := req.HTTPRequest.Header.Get("Authorization")
if auth == "" {
t.Fatal("Authorization header missing after Sign()")
}

idx := strings.Index(auth, "SignedHeaders=")
if idx == -1 {
t.Fatalf("Authorization header has no SignedHeaders: %s", auth)
}
signedHeaders := auth[idx:]
if end := strings.Index(signedHeaders, ","); end != -1 {
signedHeaders = signedHeaders[:end]
}

for _, want := range []string{"x-sdk-invocation-id", "x-sdk-request"} {
if !strings.Contains(signedHeaders, want) {
t.Errorf("SignedHeaders = %q, want it to contain %q", signedHeaders, want)
}
}
}
25 changes: 25 additions & 0 deletions volcengine/corehandlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ type lener interface {
Len() int
}

const (
// retryInvocationIDHeader carries an id that is generated once per
// logical request and stays the same across all of its retry attempts,
// so a server can group attempts that belong to the same call.
retryInvocationIDHeader = "X-Sdk-Invocation-Id"

// retryAttemptHeader carries the current attempt count (1 for the
// initial try) and the maximum number of attempts the SDK will make,
// so a server can tell a retry attempt apart from the initial request.
retryAttemptHeader = "X-Sdk-Request"
)

// AddRetryInfoHeaderHandler annotates every HTTP attempt with the request's
// invocation id and its attempt/max-attempt count. It runs on every retry
// (it is registered on Handlers.Sign, which is re-run for each attempt),
// so the attempt count in the header always reflects the current try.
var AddRetryInfoHeaderHandler = request.NamedHandler{
Name: "core.AddRetryInfoHeaderHandler",
Fn: func(r *request.Request) {
r.HTTPRequest.Header.Set(retryInvocationIDHeader, r.InvocationID)
r.HTTPRequest.Header.Set(retryAttemptHeader,
fmt.Sprintf("attempt=%d; max=%d", r.RetryCount+1, r.MaxRetries()+1))
},
}

// BuildContentLengthHandler builds the content length of a request based on the volcenginebody,
// or will use the HTTPRequest.Header's "Content-Length" if defined. If unable
// to determine request volcenginebody length and no "Content-Length" was specified it will panic.
Expand Down
1 change: 1 addition & 0 deletions volcengine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func Handlers() request.Handlers {
handlers.Build.PushBackNamed(corehandlers.CustomerRequestHandler)
handlers.Build.AfterEachFn = request.HandlerListStopOnError
handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler)
handlers.Sign.PushBackNamed(corehandlers.AddRetryInfoHeaderHandler)
handlers.Send.PushBackNamed(corehandlers.ValidateReqSigHandler)
handlers.Send.PushBackNamed(corehandlers.SendHandler)
handlers.AfterRetry.PushBackNamed(corehandlers.AfterRetryHandler)
Expand Down
65 changes: 65 additions & 0 deletions volcengine/defaults/retry_info_header_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package defaults_test

// Verifies the retry attempt headers added to volcengine/corehandlers:
// X-Sdk-Invocation-Id must stay the same across every retry attempt of a single
// logical request, and X-Sdk-Request must report the current/max attempt count.

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/volcengine/volcengine-go-sdk/volcengine/client"
"github.com/volcengine/volcengine-go-sdk/volcengine/client/metadata"
"github.com/volcengine/volcengine-go-sdk/volcengine/defaults"
"github.com/volcengine/volcengine-go-sdk/volcengine/request"
)

func TestAddRetryInfoHeaderAcrossRetries(t *testing.T) {
var invocationIDs []string
var attemptHeaders []string
callCount := 0

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
invocationIDs = append(invocationIDs, r.Header.Get("X-Sdk-Invocation-Id"))
attemptHeaders = append(attemptHeaders, r.Header.Get("X-Sdk-Request"))
if callCount < 3 {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

cfg := defaults.Config().WithHTTPClient(server.Client()).WithRegion("test-region")
handlers := defaults.Handlers()

c := client.New(*cfg, metadata.ClientInfo{Endpoint: server.URL, SigningRegion: "test-region"}, handlers)
c.Retryer = client.DefaultRetryer{NumMaxRetries: 2}

req := c.NewRequest(&request.Operation{Name: "Test", HTTPMethod: "POST", HTTPPath: "/"}, &struct{}{}, &struct{}{})
if err := req.Send(); err != nil {
t.Fatalf("expected success after retries, got error: %v", err)
}

if callCount != 3 {
t.Fatalf("expected 3 attempts (1 initial + 2 retries), got %d", callCount)
}

for i, id := range invocationIDs {
if id == "" {
t.Fatalf("attempt %d: X-Sdk-Invocation-Id header missing", i+1)
}
if id != invocationIDs[0] {
t.Fatalf("attempt %d: X-Sdk-Invocation-Id changed across retries: %q vs %q", i+1, id, invocationIDs[0])
}
}

wantAttempts := []string{"attempt=1; max=3", "attempt=2; max=3", "attempt=3; max=3"}
for i, want := range wantAttempts {
if attemptHeaders[i] != want {
t.Errorf("attempt %d: X-Sdk-Request = %q, want %q", i+1, attemptHeaders[i], want)
}
}
}
58 changes: 33 additions & 25 deletions volcengine/request/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"strings"
"time"

"github.com/google/uuid"

"github.com/volcengine/volcengine-go-sdk/volcengine"
"github.com/volcengine/volcengine-go-sdk/volcengine/custom"
"github.com/volcengine/volcengine-go-sdk/volcengine/response"
Expand Down Expand Up @@ -50,19 +52,24 @@ type Request struct {
Handlers Handlers

Retryer
AttemptTime time.Time
Time time.Time
Operation *Operation
HTTPRequest *http.Request
HTTPResponse *http.Response
Body io.ReadSeeker
BodyStart int64 // offset from beginning of Body that the request volcenginebody starts
Params interface{}
Error error
Data interface{}
RequestID string
RetryCount int
Retryable *bool
AttemptTime time.Time
Time time.Time
Operation *Operation
HTTPRequest *http.Request
HTTPResponse *http.Response
Body io.ReadSeeker
BodyStart int64 // offset from beginning of Body that the request volcenginebody starts
Params interface{}
Error error
Data interface{}
RequestID string
RetryCount int
Retryable *bool
// InvocationID identifies a single logical Send() call: it is generated
// once in New() and stays the same across all retry attempts of that
// call, letting the server tell attempts of the same request apart from
// unrelated ones.
InvocationID string
RetryDelay time.Duration
NotHoist bool
SignedHeaderVals http.Header
Expand Down Expand Up @@ -136,18 +143,19 @@ func New(cfg volcengine.Config, clientInfo metadata.ClientInfo, handlers Handler
SanitizeHostForHeader(httpReq)

r := &Request{
Config: cfg,
ClientInfo: clientInfo,
Handlers: handlers.Copy(),
Retryer: retryer,
Time: time.Now(),
ExpireTime: 0,
Operation: operation,
HTTPRequest: httpReq,
Body: nil,
Params: params,
Error: err,
Data: data,
Config: cfg,
ClientInfo: clientInfo,
Handlers: handlers.Copy(),
Retryer: retryer,
Time: time.Now(),
ExpireTime: 0,
Operation: operation,
HTTPRequest: httpReq,
Body: nil,
Params: params,
Error: err,
Data: data,
InvocationID: uuid.New().String(),
}
r.SetBufferBody([]byte{})

Expand Down