Skip to content
Merged
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
18 changes: 18 additions & 0 deletions internal/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,11 +804,29 @@ func (h *Handler) executeRequest(ctx context.Context, method, path string, req *
statusCode := 200
if err != nil {
statusCode, response = h.handleRequestError(err)
corsHeaders = errorResponseHeaders(err, corsHeaders)
}

return h.buildResponse(statusCode, corsHeaders, response, nil), nil
}

// errorResponseHeaders returns base plus any headers a client error carries
// (for example Retry-After). base is never mutated.
func errorResponseHeaders(err error, base map[string]string) map[string]string {
ce, ok := IsClientError(err)
if !ok || len(ce.headers) == 0 {
return base
}
merged := make(map[string]string, len(base)+len(ce.headers))
for k, v := range base {
merged[k] = v
}
for k, v := range ce.headers {
merged[k] = v
}
return merged
}

// handleRequestError converts an error to status code and response.
func (h *Handler) handleRequestError(err error) (int, any) { //nolint:gocritic // unnamedResult: stable internal API, adding names would require body renaming in impl
if IsNotFoundError(err) {
Expand Down
3 changes: 3 additions & 0 deletions internal/api/handler_insurance.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"

"github.com/LeanerCloud/cloud-commitments-go/pkg/insurance"
"github.com/aws/aws-lambda-go/events"

"github.com/LeanerCloud/cloud-commitments-platform/internal/archera"
Expand All @@ -11,6 +12,8 @@ import (
// InsuranceProvider is the Archera comparison surface. nil means not wired.
type InsuranceProvider interface {
Status() archera.Status
PlanID() string
Client(ctx context.Context) (insurance.QuoteClient, error)
}

// getInsuranceStatus reports whether the Archera comparison settings are
Expand Down
92 changes: 92 additions & 0 deletions internal/api/handler_insurance_comparison.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package api

import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"

"github.com/LeanerCloud/cloud-commitments-go/pkg/insurance"
"github.com/LeanerCloud/cloud-commitments-go/pkg/logging"
"github.com/aws/aws-lambda-go/events"

"github.com/LeanerCloud/cloud-commitments-platform/internal/archera"
)

// maxComparisonResponseBytes bounds the JSON returned to the browser. A larger
// comparison fails loudly rather than dropping rows.
const maxComparisonResponseBytes = 5 << 20

// getInsuranceComparison runs the explicit, read-only Archera comparison for
// the configured plan. Same gate as the status route. Only mapped, fixed-text
// errors leave this function; vendor text and the key never reach the response
// or the log.
func (h *Handler) getInsuranceComparison(ctx context.Context, req *events.LambdaFunctionURLRequest) (*archera.ComparisonDTO, error) {
if err := h.requireInsuranceAccess(ctx, req); err != nil {
return nil, err
}
if h.insurance == nil {
return nil, notConfiguredError(archera.Settings{}.Missing())
}
if st := h.insurance.Status(); !st.Configured {
return nil, notConfiguredError(st.Missing)
}
// The raw client lives in this local only.
client, err := h.insurance.Client(ctx)
if err != nil {
if errors.Is(err, archera.ErrNotConfigured) {
return nil, notConfiguredError(h.insurance.Status().Missing)
}
return nil, NewClientError(503, "Archera API key could not be resolved from "+archera.EnvKeySecret)
}
cmp, err := client.Comparison(ctx, insurance.ComparisonRequest{PlanID: h.insurance.PlanID()})
if err != nil {
return nil, mapInsuranceError(err)
}
dto, err := archera.BuildComparison(cmp)
if err != nil {
logging.Warnf("archera comparison: a vendor value has no exact decimal form")
return nil, NewClientError(502, "Archera returned a value that cannot be shown exactly")
}
raw, err := json.Marshal(dto)
if err != nil || len(raw) > maxComparisonResponseBytes {
return nil, NewClientError(502, "comparison too large to return")
}
return dto, nil
}

func notConfiguredError(missing []string) error {
return NewClientError(503, "Archera comparison is not configured; missing settings: "+strings.Join(missing, ", "))
}

// mapInsuranceError converts a pkg/insurance error to a fixed client error.
// Only the status code and Retry-After are logged; HTTPError.Message is vendor
// text and is never returned or logged.
func mapInsuranceError(err error) error {
var he *insurance.HTTPError
if !errors.As(err, &he) {
logging.Warnf("archera comparison failed: non-HTTP error")
return NewClientError(502, "Archera comparison failed")
}
logging.Warnf("archera comparison failed: http_status=%d retry_after=%s", he.StatusCode, he.RetryAfter)
switch {
case he.StatusCode == 401 || he.StatusCode == 403:
return NewClientError(502, "Archera rejected the configured credentials")
case he.StatusCode == 404:
return NewClientError(502, "Archera organization or plan was not found")
case he.StatusCode == 429:
if he.RetryAfter <= 0 {
return NewClientError(429, "Archera rate limit reached; retry-after not given")
}
secs := int((he.RetryAfter + time.Second - 1) / time.Second)
return NewClientErrorWithHeaders(429, "Archera rate limit reached",
map[string]any{"retry_after_seconds": secs},
map[string]string{"Retry-After": strconv.Itoa(secs)})
case he.StatusCode >= 500:
return NewClientError(502, "Archera service error")
}
return NewClientError(502, fmt.Sprintf("Archera request failed with HTTP %d", he.StatusCode))
}
Loading
Loading