Files
nezha_domains/integration/agentcompat/internal/client/http.go
T
2026-07-20 04:48:15 +00:00

145 lines
3.9 KiB
Go

package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/nezhahq/nezha/pkg/agentcompatcontract"
)
type CommonResponse[T any] struct {
Success bool `json:"success"`
Data T `json:"data"`
Error string `json:"error"`
}
type RESTRequest[T any] struct {
Method string
Path string
Body *T
IOStreamCapability IOStreamCapability
}
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
Expire string `json:"expire"`
}
func (client *Client) Login(ctx context.Context, request LoginRequest) (LoginResponse, error) {
return REST[LoginRequest, LoginResponse](ctx, client, RESTRequest[LoginRequest]{
Method: http.MethodPost,
Path: "/api/v1/login",
Body: &request,
})
}
func REST[Request, Response any](ctx context.Context, client *Client, request RESTRequest[Request]) (Response, error) {
var zero Response
requestURL, err := client.resolvePath(request.Path)
if err != nil {
return zero, err
}
var body io.Reader
if request.Body != nil {
encoded, marshalErr := json.Marshal(request.Body)
if marshalErr != nil {
return zero, fmt.Errorf("encode REST request: %w", marshalErr)
}
body = bytes.NewReader(encoded)
}
requestContext, cancel := client.requestContext(ctx)
defer cancel()
httpRequest, err := http.NewRequestWithContext(requestContext, request.Method, requestURL.String(), body)
if err != nil {
return zero, fmt.Errorf("create REST request: %w", err)
}
if request.Body != nil {
httpRequest.Header.Set("Content-Type", "application/json")
}
client.applyAuthenticatedHeaders(httpRequest, true)
if request.IOStreamCapability.Value() != "" {
httpRequest.Header.Set(agentcompatcontract.IOStreamCapabilityHeader, request.IOStreamCapability.Value())
}
status, responseBody, err := client.execute(httpRequest, client.maxResponseBytes)
if err != nil {
return zero, err
}
if status < 200 || status >= 300 {
var envelope CommonResponse[Response]
if json.Unmarshal(responseBody, &envelope) == nil {
return zero, &HTTPError{StatusCode: status, Message: Redact(envelope.Error)}
}
return zero, &HTTPError{StatusCode: status, Message: Redact(string(responseBody))}
}
var envelope CommonResponse[Response]
if err := json.Unmarshal(responseBody, &envelope); err != nil {
return zero, fmt.Errorf("decode REST response: %w", err)
}
if !envelope.Success {
return zero, fmt.Errorf("%w: %s", ErrSemanticFailure, Redact(envelope.Error))
}
return envelope.Data, nil
}
func DoREST[Request, Response any](ctx context.Context, client *Client, request RESTRequest[Request]) (Response, error) {
return REST[Request, Response](ctx, client, request)
}
func (client *Client) execute(request *http.Request, maxBytes int64) (int, []byte, error) {
client.requestCount.Add(1)
response, err := client.httpClient.Do(request)
if err != nil {
if request.Context().Err() != nil {
return 0, nil, fmt.Errorf("HTTP request: %w", request.Context().Err())
}
return 0, nil, errorsNewRedacted("HTTP request", err)
}
defer response.Body.Close()
body, err := readBounded(response.Body, maxBytes)
if err != nil {
return response.StatusCode, nil, err
}
return response.StatusCode, body, nil
}
func readBounded(reader io.Reader, maxBytes int64) ([]byte, error) {
body, err := io.ReadAll(io.LimitReader(reader, maxBytes+1))
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}
if int64(len(body)) > maxBytes {
return nil, ErrResponseTooLarge
}
return body, nil
}
func errorsNewRedacted(operation string, err error) error {
return &redactedOperationError{operation: operation, cause: err}
}
type redactedOperationError struct {
operation string
cause error
}
func (err *redactedOperationError) Error() string {
return fmt.Sprintf("%s: %s", err.operation, Redact(err.cause.Error()))
}
func (err *redactedOperationError) Unwrap() error {
return err.cause
}