Errors

Section: Warlot Go SDK

Errors

A consistent error model simplifies operational handling across transport failures, non-success HTTP statuses, and SQL runtime issues. This page documents error surfaces, the SDK’s APIError type, retry boundaries, recommended handling patterns, and compact unit-test definitions.


Error surfaces

LayerExample symptomReturned asNotes
Transport / contextDNS/TLS/connect timeout, context deadline exceedederror (e.g., context.DeadlineExceeded)No HTTP response observed. Retries may occur depending on failure timing.
HTTP non-2xx401/403/404/429/5xx*warlot.APIErrorBody is parsed for message, error, code, details.
Successful HTTP, JSON decodeType mismatch, malformed JSONerror (wrap includes decode response:)Includes original body (truncated only by logs), no retry.
SQL runtime (200 with ok=false){ "ok": false, "error": "…" }error (SDK returns error containing message)Occurs when service reports statement-level failure.
Streaming readPremature close, malformed arrayRowScanner.Err()After Next returns false, check Err() to distinguish EOF vs. error.

Unified error type for non-2xx

GO
type APIError struct {
    StatusCode int
    Body       string
    Message    string
    Code       string      // optional code provided by gateway
    Details    interface{} // optional details payload
}

func (e *APIError) Error() string

Decoding behavior

  • The SDK attempts to unmarshal the response body into { message, error, code, details }.
  • Message is populated from message (preferred) or error fields when present.
  • Body always retains the raw text, aiding diagnostics.

Retry boundaries

  • Automatic retries occur for 429 and 5xx responses.
  • Retry-After (seconds or HTTP-date) is honored; otherwise, a jittered exponential backoff is applied within configured bounds.
  • Non-retriable statuses (for example, 400/401/403/404) are returned immediately.
  • Transport-level retries may occur depending on error timing and idempotent request safety.

For write statements (INSERT/UPDATE/DELETE/DDL), idempotency keys should be set to prevent duplicate effects during retries.


Status mapping and recommended action

HTTP statusMeaningSDK behaviorRecommended action
400 Bad RequestInvalid SQL or parametersAPIError (no retry)Validate SQL/params; correct request.
401 UnauthorizedMissing/invalid x-api-keyAPIError (no retry)Issue or refresh API key; attach to client.
403 ForbiddenProject/holder scope mismatchAPIError (no retry)Confirm holder_id/project_name and key scope.
404 Not FoundResource/route absentAPIError (no retry)Confirm endpoint and project ID; consider resolve/init flow.
409 ConflictState conflictAPIError (no retry)Reconcile state; for writes, prefer idempotency keys.
429 Too Many RequestsRate limitRetry with backoff (honors Retry-After)Allow SDK to retry; consider idempotency on writes and tuning backoff.
5xx Server ErrorTransient backend errorRetry with backoffAllow SDK to retry; escalate if persistent.

Handling patterns

Distinguish API errors from other errors

GO
res, err := proj.SQL(ctx, `SELECT id FROM t`, nil)
if err != nil {
    if e, ok := err.(*warlot.APIError); ok {
        switch e.StatusCode {
        case 401, 403:
            // Re-issue key or correct scope headers.
        case 429:
            // Request already retried; consider backoff tuning.
        default:
            // Log e.Code/e.Message/e.Body for diagnostics.
        }
        return
    }
    // Transport, decode, or SQL runtime error.
    return
}
// use res.Rows / res.RowCount

Context timeouts

GO
cctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
_, err := proj.SQL(cctx, `SELECT 1`, nil)
if errors.Is(err, context.DeadlineExceeded) {
    // Increase timeout or reduce workload.
}

Streaming errors

GO
sc, err := client.ExecSQLStream(ctx, projectID, warlot.SQLRequest{SQL: `SELECT * FROM big_table`})
if err != nil { return err }
defer sc.Close()

var row map[string]any
for sc.Next(&row) {
    // process
}
if err := sc.Err(); err != nil {
    // Handle malformed JSON or early disconnect.
}

Idempotency on writes

GO
_, err := proj.SQL(ctx,
    `INSERT INTO audit(event) VALUES (?)`,
    []any{"create-123"},
    warlot.WithIdempotencyKey("audit-create-123"),
)

Logging considerations

  • The SDK’s optional logger redacts x-api-key.
  • For sensitive environments, consider suppressing full response bodies; rely on Message, Code, and structured Details.

Types (definitions)

Relevant selections for error handling:

GO
// Public error type for non-2xx responses.
type APIError struct {
    StatusCode int
    Body       string
    Message    string
    Code       string
    Details    any
}

// ExecSQL returns:
//   - *SQLResponse with OK=true for success,
//   - error when ok=false (SQL-reported error) or on any failure.
func (c *Client) ExecSQL(ctx context.Context, projectID string, req SQLRequest, opts ...CallOption) (*SQLResponse, error)

// Streaming scanner; terminal error accessed via Err().
type RowScanner struct {/* … */}
func (s *RowScanner) Next(dst any) bool
func (s *RowScanner) Err() error
func (s *RowScanner) Close() error

Unit test definitions

1) Non-2xx produces APIError with parsed message

GO
// errors_apierror_parse_test.go (package warlot)
package warlot

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

func Test_APIError_ParsesMessage(t *testing.T) {
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, `{"message":"forbidden","code":"FORBIDDEN"}`, http.StatusForbidden)
	}))
	defer s.Close()

	cl := New(WithBaseURL(s.URL))
	_, err := cl.ExecSQL(context.Background(), "P", SQLRequest{SQL: "SELECT 1"})
	if err == nil {
		t.Fatalf("expected error")
	}
	e, ok := err.(*APIError)
	if !ok || e.StatusCode != 403 || e.Message != "forbidden" || e.Code != "FORBIDDEN" {
		t.Fatalf("unexpected APIError: %#v", err)
	}
}

2) 429 is retried then succeeds

GO
// errors_retry_429_test.go (package warlot)
package warlot

import (
	"context"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
)

func Test_Retry_429_ThenSuccess(t *testing.T) {
	var calls int32
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if atomic.AddInt32(&calls, 1) == 1 {
            w.Header().Set("Retry-After", "1")
			http.Error(w, `{"message":"rate limited"}`, http.StatusTooManyRequests)
			return
		}
		w.WriteHeader(200)
		w.Write([]byte(`{"ok":true,"row_count":0}`))
	}))
	defer s.Close()

	cl := New(WithBaseURL(s.URL), WithRetries(2))
	if _, err := cl.ExecSQL(context.Background(), "P", SQLRequest{SQL: "CREATE TABLE t(x)"}); err != nil {
		t.Fatalf("unexpected failure after retry: %v", err)
	}
}

3) Decode error is propagated with context

GO
// errors_decode_test.go (package warlot)
package warlot

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func Test_DecodeError_IsReturned(t *testing.T) {
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// invalid JSON
		w.WriteHeader(200)
		w.Write([]byte(`{"ok":true,"row_count":"not-an-int"}`))
	}))
	defer s.Close()

	cl := New(WithBaseURL(s.URL))
	_, err := cl.ExecSQL(context.Background(), "P", SQLRequest{SQL: "CREATE TABLE t(x)"} )
	if err == nil || !strings.Contains(err.Error(), "decode response") {
		t.Fatalf("expected decode error, got %v", err)
	}
}

4) Context deadline surfaces as context.DeadlineExceeded

GO
// errors_context_timeout_test.go (package warlot)
package warlot

import (
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func Test_ContextDeadlineExceeded(t *testing.T) {
	// Simulate a slow handler that outlives the context timeout.
	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(200 * time.Millisecond)
		w.WriteHeader(200)
		w.Write([]byte(`{"ok":true,"row_count":0}`))
	}))
	defer s.Close()

	cl := New(WithBaseURL(s.URL))
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer cancel()

	_, err := cl.ExecSQL(ctx, "P", SQLRequest{SQL: "CREATE TABLE t(x)"} )
	if err == nil || ctx.Err() == nil {
		t.Fatalf("expected context deadline error, got %v", err)
	}
}

Troubleshooting

SymptomLikely causeAction
APIError with 401/403Missing key or scope mismatchIssue key; confirm holder/project headers.
json: cannot unmarshal …Upstream shape differs from expectationUse untyped row access or adjust struct field types/tags.
Persistent 429 despite retriesHigh request rate or tight limitsIncrease backoff, reduce concurrency, ensure idempotency on writes.
Streaming terminates earlyNetwork interruption or malformed arrayInspect RowScanner.Err(); retry operation or adjust workload chunking.

Related topics