mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 08:12:36 +08:00
Compare commits
2 Commits
feat/batch
...
fix/ppe-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b81f7ae4 | ||
|
|
5eaeab2bb3 |
@@ -153,9 +153,79 @@ func (c *APIClient) DoSDKRequest(ctx context.Context, req *larkcore.ApiReq, as c
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, WrapDoAPIError(err)
|
return nil, WrapDoAPIError(err)
|
||||||
}
|
}
|
||||||
|
c.logAPIResponse(req, resp)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) logAPIResponse(req *larkcore.ApiReq, resp *larkcore.ApiResp) {
|
||||||
|
if resp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logID := strings.TrimSpace(resp.LogId())
|
||||||
|
if logID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
method, path := apiReqLogFields(req, "")
|
||||||
|
fmt.Fprintf(c.errOut(), "[lark-cli] api-response: method=%s path=%s status=%d log_id=%s\n", method, path, resp.StatusCode, logID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) logStreamResponse(req *larkcore.ApiReq, requestURL string, resp *http.Response) {
|
||||||
|
if resp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logID := streamLogID(resp.Header)
|
||||||
|
if logID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
method, path := apiReqLogFields(req, requestURL)
|
||||||
|
fmt.Fprintf(c.errOut(), "[lark-cli] api-response: method=%s path=%s status=%d log_id=%s\n", method, path, resp.StatusCode, logID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) errOut() io.Writer {
|
||||||
|
if c != nil && c.ErrOut != nil {
|
||||||
|
return c.ErrOut
|
||||||
|
}
|
||||||
|
return io.Discard
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiReqLogFields(req *larkcore.ApiReq, fallbackURL string) (string, string) {
|
||||||
|
method := ""
|
||||||
|
path := ""
|
||||||
|
if req != nil {
|
||||||
|
method = req.HttpMethod
|
||||||
|
path = req.ApiPath
|
||||||
|
}
|
||||||
|
method = strings.ToUpper(strings.TrimSpace(method))
|
||||||
|
if method == "" {
|
||||||
|
method = "UNKNOWN"
|
||||||
|
}
|
||||||
|
path = requestLogPath(path)
|
||||||
|
if path == "missing" {
|
||||||
|
path = requestLogPath(fallbackURL)
|
||||||
|
}
|
||||||
|
return method, path
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestLogPath(raw string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return "missing"
|
||||||
|
}
|
||||||
|
if u, err := url.Parse(raw); err == nil && u.IsAbs() {
|
||||||
|
if u.EscapedPath() != "" {
|
||||||
|
return u.EscapedPath()
|
||||||
|
}
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
if i := strings.Index(raw, "?"); i >= 0 {
|
||||||
|
raw = raw[:i]
|
||||||
|
}
|
||||||
|
if raw == "" {
|
||||||
|
return "missing"
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
// DoStream executes a streaming HTTP request against the Lark OpenAPI endpoint.
|
// DoStream executes a streaming HTTP request against the Lark OpenAPI endpoint.
|
||||||
// Unlike DoSDKRequest (which buffers the full body via the SDK), DoStream returns
|
// Unlike DoSDKRequest (which buffers the full body via the SDK), DoStream returns
|
||||||
// a live *http.Response whose Body is an io.Reader for streaming consumption.
|
// a live *http.Response whose Body is an io.Reader for streaming consumption.
|
||||||
@@ -224,6 +294,7 @@ func (c *APIClient) DoStream(ctx context.Context, req *larkcore.ApiReq, as core.
|
|||||||
return nil, errs.NewNetworkError(classifyNetworkSubtype(err), "stream request failed: %s", err).WithCause(err)
|
return nil, errs.NewNetworkError(classifyNetworkSubtype(err), "stream request failed: %s", err).WithCause(err)
|
||||||
}
|
}
|
||||||
resp.Body = &cancelOnCloseBody{ReadCloser: resp.Body, cancel: cancel}
|
resp.Body = &cancelOnCloseBody{ReadCloser: resp.Body, cancel: cancel}
|
||||||
|
c.logStreamResponse(req, requestURL, resp)
|
||||||
|
|
||||||
// Handle HTTP errors internally
|
// Handle HTTP errors internally
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
|
|||||||
@@ -464,6 +464,48 @@ func TestDoStream_TransportFailureSplitsSubtype(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDoStream_LogsLogIDToErrOut(t *testing.T) {
|
||||||
|
errBuf := &bytes.Buffer{}
|
||||||
|
rt := roundTripFunc(func(_ *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Header: http.Header{
|
||||||
|
"Content-Type": []string{"application/octet-stream"},
|
||||||
|
larkcore.HttpHeaderKeyLogId: []string{"stream-log-123"},
|
||||||
|
},
|
||||||
|
Body: io.NopCloser(strings.NewReader("ok")),
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
ac := &APIClient{
|
||||||
|
HTTP: &http.Client{Transport: rt},
|
||||||
|
ErrOut: errBuf,
|
||||||
|
Credential: credential.NewCredentialProvider(nil, nil, &staticTokenResolver{}, nil),
|
||||||
|
Config: &core.CliConfig{AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ac.DoStream(context.Background(), &larkcore.ApiReq{
|
||||||
|
HttpMethod: http.MethodGet,
|
||||||
|
ApiPath: "/open-apis/drive/v1/medias/file_token/download",
|
||||||
|
}, core.AsBot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DoStream() error = %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
got := errBuf.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
"[lark-cli] api-response:",
|
||||||
|
"method=GET",
|
||||||
|
"path=/open-apis/drive/v1/medias/file_token/download",
|
||||||
|
"status=200",
|
||||||
|
"log_id=stream-log-123",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Fatalf("log missing %q; got:\n%s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// failingTokenResolver always returns TokenUnavailableError, exercising the
|
// failingTokenResolver always returns TokenUnavailableError, exercising the
|
||||||
// auth/credential failure path through resolveAccessToken.
|
// auth/credential failure path through resolveAccessToken.
|
||||||
type failingTokenResolver struct{}
|
type failingTokenResolver struct{}
|
||||||
@@ -618,6 +660,41 @@ func TestDoSDKRequest_TransportFailureWrapsAsNetwork(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDoSDKRequest_LogsLogIDToErrOut(t *testing.T) {
|
||||||
|
rt := roundTripFunc(func(_ *http.Request) (*http.Response, error) {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Header: http.Header{
|
||||||
|
"Content-Type": []string{"application/json"},
|
||||||
|
larkcore.HttpHeaderKeyLogId: []string{"sdk-log-123"},
|
||||||
|
},
|
||||||
|
Body: io.NopCloser(strings.NewReader(`{"code":0,"msg":"ok","data":{}}`)),
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
ac, errBuf := newTestAPIClient(t, rt)
|
||||||
|
|
||||||
|
_, err := ac.DoSDKRequest(context.Background(), &larkcore.ApiReq{
|
||||||
|
HttpMethod: http.MethodGet,
|
||||||
|
ApiPath: "/open-apis/contact/v3/users/me",
|
||||||
|
}, core.AsBot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DoSDKRequest() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := errBuf.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
"[lark-cli] api-response:",
|
||||||
|
"method=GET",
|
||||||
|
"path=/open-apis/contact/v3/users/me",
|
||||||
|
"status=200",
|
||||||
|
"log_id=sdk-log-123",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Fatalf("log missing %q; got:\n%s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestCallAPI_ParseJSONFailureWrapsAsAPI pins the typed-envelope contract for
|
// TestCallAPI_ParseJSONFailureWrapsAsAPI pins the typed-envelope contract for
|
||||||
// malformed JSON response bodies: WrapJSONResponseParseError emits
|
// malformed JSON response bodies: WrapJSONResponseParseError emits
|
||||||
// *errs.InternalError{Subtype: invalid_response} with the rawAPIJSONHint
|
// *errs.InternalError{Subtype: invalid_response} with the rawAPIJSONHint
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ const (
|
|||||||
HeaderShortcut = "X-Cli-Shortcut"
|
HeaderShortcut = "X-Cli-Shortcut"
|
||||||
HeaderExecutionId = "X-Cli-Execution-Id"
|
HeaderExecutionId = "X-Cli-Execution-Id"
|
||||||
HeaderAgentTrace = "X-Agent-Trace"
|
HeaderAgentTrace = "X-Agent-Trace"
|
||||||
|
HeaderTTEnv = "X-Tt-Env"
|
||||||
|
HeaderUsePPE = "X-Use-Ppe"
|
||||||
|
HeaderRPCAppID = "Rpc-Persist-Cli-Req-App-Id"
|
||||||
|
|
||||||
SourceValue = "lark-cli"
|
SourceValue = "lark-cli"
|
||||||
|
TTEnvValue = "ppe_doubao_office_local"
|
||||||
|
UsePPEValue = "1"
|
||||||
|
RPCAppID = "497858"
|
||||||
|
|
||||||
HeaderUserAgent = "User-Agent"
|
HeaderUserAgent = "User-Agent"
|
||||||
|
|
||||||
@@ -75,6 +81,9 @@ func BaseSecurityHeaders() http.Header {
|
|||||||
h.Set(HeaderVersion, build.Version)
|
h.Set(HeaderVersion, build.Version)
|
||||||
h.Set(HeaderBuild, DetectBuildKind())
|
h.Set(HeaderBuild, DetectBuildKind())
|
||||||
h.Set(HeaderUserAgent, UserAgentValue())
|
h.Set(HeaderUserAgent, UserAgentValue())
|
||||||
|
h.Set(HeaderTTEnv, TTEnvValue)
|
||||||
|
h.Set(HeaderUsePPE, UsePPEValue)
|
||||||
|
h.Set(HeaderRPCAppID, RPCAppID)
|
||||||
if v := AgentTraceValue(); v != "" {
|
if v := AgentTraceValue(); v != "" {
|
||||||
h.Set(HeaderAgentTrace, v)
|
h.Set(HeaderAgentTrace, v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,13 +256,26 @@ func TestBaseSecurityHeaders_IncludesBuildHeader(t *testing.T) {
|
|||||||
|
|
||||||
func TestBaseSecurityHeaders_AllRequiredHeaders(t *testing.T) {
|
func TestBaseSecurityHeaders_AllRequiredHeaders(t *testing.T) {
|
||||||
h := BaseSecurityHeaders()
|
h := BaseSecurityHeaders()
|
||||||
for _, key := range []string{HeaderSource, HeaderVersion, HeaderBuild, HeaderUserAgent} {
|
for _, key := range []string{HeaderSource, HeaderVersion, HeaderBuild, HeaderUserAgent, HeaderTTEnv, HeaderUsePPE, HeaderRPCAppID} {
|
||||||
if h.Get(key) == "" {
|
if h.Get(key) == "" {
|
||||||
t.Errorf("BaseSecurityHeaders missing %s", key)
|
t.Errorf("BaseSecurityHeaders missing %s", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBaseSecurityHeaders_IncludesPersistentRequestHeaders(t *testing.T) {
|
||||||
|
h := BaseSecurityHeaders()
|
||||||
|
if got := h.Get(HeaderTTEnv); got != TTEnvValue {
|
||||||
|
t.Fatalf("BaseSecurityHeaders()[%s] = %q, want %q", HeaderTTEnv, got, TTEnvValue)
|
||||||
|
}
|
||||||
|
if got := h.Get(HeaderUsePPE); got != UsePPEValue {
|
||||||
|
t.Fatalf("BaseSecurityHeaders()[%s] = %q, want %q", HeaderUsePPE, got, UsePPEValue)
|
||||||
|
}
|
||||||
|
if got := h.Get(HeaderRPCAppID); got != RPCAppID {
|
||||||
|
t.Fatalf("BaseSecurityHeaders()[%s] = %q, want %q", HeaderRPCAppID, got, RPCAppID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// AgentTraceValue / HeaderAgentTrace
|
// AgentTraceValue / HeaderAgentTrace
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user