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
1 change: 0 additions & 1 deletion cmd/lk/testdata/script/rejected-session.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ cp stderr lk.stderr
requests
cmp stdout want-requests

skip 'known bug: lk shows the raw body of the Public API''s 401 instead of its message'
cmp lk.stderr want-stderr

-- home/.livekit/cli-config.yaml --
Expand Down
21 changes: 17 additions & 4 deletions pkg/public/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,28 @@ func IsNotFound(err error) bool {
}

// responseError builds an APIError from a non-2xx response, preferring the
// API's error envelope ({"code","message","details"}) and falling back
// to the raw body.
// API's error envelope ({"code","message","details"}, or the
// {"error":{"code","message"}} its auth and panic-recovery middleware answer
// with) and falling back to the raw body.
func responseError(status int, body []byte) error {
var env struct {
Message string `json:"message"`
Details []json.RawMessage `json:"details"`
Error json.RawMessage `json:"error"`
}
if err := json.Unmarshal(body, &env); err == nil && env.Message != "" {
return &APIError{Status: status, Message: env.Message, details: env.Details}
if err := json.Unmarshal(body, &env); err == nil {
msg := env.Message
if msg == "" && len(env.Error) > 0 {
var nested struct {
Message string `json:"message"`
}
if json.Unmarshal(env.Error, &nested) == nil {
msg = nested.Message
}
}
if msg != "" {
return &APIError{Status: status, Message: msg, details: env.Details}
}
}
msg := strings.TrimSpace(string(body))
if msg == "" {
Expand Down
20 changes: 20 additions & 0 deletions pkg/public/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ func TestResponseError(t *testing.T) {
assert.Equal(t, http.StatusForbidden, apiErr.Status)
assert.Equal(t, "forbidden", apiErr.Message)

// The auth layer's 401 nests its message under "error".
err = responseError(http.StatusUnauthorized, []byte(`{"error":{"code":"unauthenticated","message":"authentication required"}}`))
require.ErrorAs(t, err, &apiErr)
assert.Equal(t, http.StatusUnauthorized, apiErr.Status)
assert.Equal(t, "authentication required", apiErr.Message)

// A top-level message wins over a nested one.
err = responseError(http.StatusBadRequest, []byte(`{"message":"top","error":{"message":"nested"}}`))
require.ErrorAs(t, err, &apiErr)
assert.Equal(t, "top", apiErr.Message)

// A nested error without a message, or an "error" that isn't an object,
// doesn't hide the top-level message or the raw body.
err = responseError(http.StatusBadRequest, []byte(`{"error":{"code":"x"}}`))
require.ErrorAs(t, err, &apiErr)
assert.Contains(t, apiErr.Message, "400")
err = responseError(http.StatusBadRequest, []byte(`{"message":"top","error":"a string"}`))
require.ErrorAs(t, err, &apiErr)
assert.Equal(t, "top", apiErr.Message)

// Non-envelope body falls back to the raw text (with the status).
err = responseError(http.StatusInternalServerError, []byte("boom"))
require.ErrorAs(t, err, &apiErr)
Expand Down
Loading