github.com/qiniu/go-sdk/v7@v7.27.0/sandbox/client.go (about)

     1  package sandbox
     2  
     3  import (
     4  	"cmp"
     5  	"context"
     6  	"fmt"
     7  	"net/http"
     8  	"os"
     9  	"strconv"
    10  	"time"
    11  
    12  	"github.com/qiniu/go-sdk/v7/auth"
    13  	"github.com/qiniu/go-sdk/v7/sandbox/internal/apis"
    14  )
    15  
    16  // DefaultEndpoint 是沙箱 API 的默认服务地址。
    17  const DefaultEndpoint = "https://cn-yangzhou-1-sandbox.qiniuapi.com"
    18  
    19  // Config 是沙箱客户端的配置。
    20  type Config struct {
    21  	// APIKey 是用于身份认证的 API 密钥(必填)。
    22  	APIKey string
    23  
    24  	// Credentials 是用于身份认证的七牛凭证对象(可选)。
    25  	// InjectionRule 相关接口会使用 Credentials 进行认证。
    26  	Credentials *auth.Credentials
    27  
    28  	// Endpoint 是沙箱 API 服务地址(可选,默认值:DefaultEndpoint)。
    29  	Endpoint string
    30  
    31  	// HTTPClient 自定义 HTTP 客户端(可选,默认值:http.DefaultClient)。
    32  	HTTPClient *http.Client
    33  
    34  	// RetryMax 可重试 API 调用的最大重试次数(可选)。
    35  	// 设置为 0 禁用重试;nil 时读取 SANDBOX_RETRY_MAX,未设置时默认 5。
    36  	RetryMax *int
    37  
    38  	// RetryBackoff 重试退避函数,输入第几次重试(0-based),返回等待时长(可选)。
    39  	// 默认:指数退避 500ms → 10s + 随机抖动。
    40  	// 返回 0 表示不等待直接重试。
    41  	RetryBackoff func(attempt int) time.Duration
    42  }
    43  
    44  // Client 是沙箱客户端。
    45  type Client struct {
    46  	config *Config
    47  	api    apis.ClientWithResponsesInterface
    48  }
    49  
    50  // NewClient 创建一个新的沙箱客户端。
    51  func NewClient(config *Config) (*Client, error) {
    52  	if err := normalizeRetryMax(config); err != nil {
    53  		return nil, err
    54  	}
    55  	endpoint := config.Endpoint
    56  	if endpoint == "" {
    57  		endpoint = DefaultEndpoint
    58  	}
    59  	if config.HTTPClient == nil {
    60  		config.HTTPClient = http.DefaultClient
    61  	}
    62  
    63  	opts := []apis.ClientOption{
    64  		apis.WithHTTPClient(config.HTTPClient),
    65  		apis.WithRequestEditorFn(reqidEditor()),
    66  	}
    67  	if config.APIKey != "" {
    68  		opts = append(opts, apis.WithRequestEditorFn(apiKeyEditor(config.APIKey)))
    69  	}
    70  
    71  	client, err := apis.NewClientWithResponses(endpoint, opts...)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	return &Client{config: config, api: client}, nil
    77  }
    78  
    79  // normalizeRetryMax 校验重试次数,并在未显式配置时读取 SANDBOX_RETRY_MAX。
    80  func normalizeRetryMax(config *Config) error {
    81  	if config.RetryMax != nil {
    82  		if *config.RetryMax < 0 {
    83  			return fmt.Errorf("RetryMax must be a non-negative integer")
    84  		}
    85  		return nil
    86  	}
    87  	envVal := os.Getenv("SANDBOX_RETRY_MAX")
    88  	if envVal == "" {
    89  		return nil
    90  	}
    91  	maxRetries, err := strconv.Atoi(envVal)
    92  	if err != nil || maxRetries < 0 {
    93  		return fmt.Errorf("SANDBOX_RETRY_MAX must be a non-negative integer")
    94  	}
    95  	config.RetryMax = &maxRetries
    96  	return nil
    97  }
    98  
    99  // reqidEditor 返回一个 RequestEditorFn,从 context 中提取 reqid 并注入 X-Reqid 请求头。
   100  // 与 SDK 其他子产品(如 storage、media 等)的行为保持一致,方便统一链路追踪。
   101  func reqidEditor() apis.RequestEditorFn {
   102  	return func(ctx context.Context, req *http.Request) error {
   103  		setReqidHeader(ctx, req)
   104  		return nil
   105  	}
   106  }
   107  
   108  // apiKeyEditor 返回一个 RequestEditorFn,用于注入 API Key 认证头。
   109  // 同时设置 X-API-Key 和 Authorization: Bearer,兼容不同端点对认证头的要求:
   110  //   - 多数端点接受 X-API-Key
   111  //   - 部分端点(如 POST /templates/{templateID} rebuild)要求 Authorization
   112  //
   113  // 与 E2B js-sdk (packages/js-sdk/src/api/index.ts) 的双 header 行为保持一致。
   114  //
   115  // 与 Credentials 共存:apiKeyEditor 作为 client-level editor 先于 per-request
   116  // editor 执行;GetCredentialsOption 返回的 editor 会 Set 覆盖 Authorization
   117  // 为 "Qiniu <sig>",因此 Credentials 的优先级依旧生效,Bearer 只在未叠加
   118  // Credentials editor 时才会命中。此时 X-API-Key 仍会保留在请求中,服务端
   119  // 接受并忽略,与改动前 APIKey-only 路径行为一致。
   120  func apiKeyEditor(apiKey string) apis.RequestEditorFn {
   121  	return func(ctx context.Context, req *http.Request) error {
   122  		if req.Header.Get("Authorization") != "" {
   123  			// 调用方已在进入 client-level editor 链之前手动预设 Authorization
   124  			// (非常规路径),尊重其选择并跳过注入,避免覆盖调用方的认证选择。
   125  			return nil
   126  		}
   127  		req.Header.Set("X-API-Key", apiKey)
   128  		req.Header.Set("Authorization", "Bearer "+apiKey)
   129  		return nil
   130  	}
   131  }
   132  
   133  func (c *Client) GetCredentialsOption() (apis.RequestEditorFn, error) {
   134  	cred := cmp.Or(c.config.Credentials, auth.Default())
   135  	if cred == nil {
   136  		return nil, fmt.Errorf("credentials not provided in client config")
   137  	}
   138  	return func(ctx context.Context, req *http.Request) error {
   139  		token, err := cred.SignRequestV2(req)
   140  		if err != nil {
   141  			return err
   142  		}
   143  		req.Header.Set("Authorization", fmt.Sprintf("Qiniu %s", token))
   144  		return nil
   145  	}, nil
   146  }