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
6 changes: 6 additions & 0 deletions changelog.d/0025-capi-5xx-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[BugFixes]
- When a Cloud Foundry endpoint keeps answering with a server error
(5xx), the endpoint banner now says the endpoint is unreachable or
down and shows Cloud Foundry's status code, for example "HTTP 503: The
endpoint is unreachable or down.". It used to read "Request failed."
with no code.
6 changes: 4 additions & 2 deletions src/jetstream/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -245,5 +245,7 @@ require (
)

// User-provided service instance deletes answer 204 with no Location;
// fivetwenty-io/capi#16 accepts that. Drop this once a release carries it.
replace github.com/fivetwenty-io/capi/v3 => github.com/norman-abramovitz/fw-capi/v3 v3.216.7-0.20261020094728-a3625edf6e94
// fivetwenty-io/capi#16 accepts that. A CF 5xx that outlasts capi's
// retries keeps its sentinel, and every mapped error carries its HTTP
// status (capi.StatusError). Drop this once a release carries both.
replace github.com/fivetwenty-io/capi/v3 => github.com/norman-abramovitz/fw-capi/v3 v3.216.7-0.20261022071155-a4e9d752d752
4 changes: 2 additions & 2 deletions src/jetstream/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,8 @@ github.com/ncruces/go-sqlite3-wasm/v6 v6.3.35304 h1:dBSZlcEFdtBMvNRg34y50mConBPO
github.com/ncruces/go-sqlite3-wasm/v6 v6.3.35304/go.mod h1:YvoJzbJpX6phd3BGdtiXu2NuD5RX6G8zsUdzt47GgOY=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/norman-abramovitz/fw-capi/v3 v3.216.7-0.20261020094728-a3625edf6e94 h1:khj6P4JmNsiW1SSkUJMAioYePKWKXQVfJnT772ajmV4=
github.com/norman-abramovitz/fw-capi/v3 v3.216.7-0.20261020094728-a3625edf6e94/go.mod h1:jcpCj9Pz+dacd9bVg65lQzQwgAtqAlGz2e6Bt9DKCtE=
github.com/norman-abramovitz/fw-capi/v3 v3.216.7-0.20261022071155-a4e9d752d752 h1:Bg5Hdtrj56wP6XqNopXogJXCW3Y/uqL9O7Nz9LrEK0Q=
github.com/norman-abramovitz/fw-capi/v3 v3.216.7-0.20261022071155-a4e9d752d752/go.mod h1:jcpCj9Pz+dacd9bVg65lQzQwgAtqAlGz2e6Bt9DKCtE=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,11 @@ func TestAssociateUser_CFRejectionKeepsStatus(t *testing.T) {
}{
{"create forbidden (non-admin caller)", http.StatusOK, associateLookupOK, http.StatusForbidden, notAuthorized,
http.StatusForbidden, "", "403", "CF-NotAuthorized"},
// capi retries a 5xx and then gives up with a transport error
// that carries neither the sentinel nor the envelope; it stays a
// plain 502.
// A 5xx that outlasts capi's retries keeps its sentinel, envelope
// and status: a 502 that names the endpoint unreachable and
// reports CF's 503.
{"create server error", http.StatusOK, associateLookupOK, http.StatusServiceUnavailable, serverError,
http.StatusBadGateway, "", "", `"error":`},
http.StatusBadGateway, "unreachable", "503", "CF-ServerError"},
{"lookup forbidden", http.StatusForbidden, notAuthorized, http.StatusCreated, `{}`,
http.StatusForbidden, "", "403", "CF-NotAuthorized"},
}
Expand Down
19 changes: 12 additions & 7 deletions src/jetstream/plugins/cloudfoundry/native_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import (
)

// capiStatusRe matches the "(status NNN)" fragment capi.MapHTTPError embeds in
// its error messages. capi sentinel errors don't expose the numeric HTTP
// status via a field, so this is the only way to recover it for display.
// its non-envelope error messages. It recovers the status from an error
// whose wrap chain no longer reaches the *capi.StatusError.
var capiStatusRe = regexp.MustCompile(`\(status (\d{3})\)`)

// gorouterRouteMissingRe matches the body gorouter returns when the platform's
Expand Down Expand Up @@ -175,11 +175,12 @@ func diagFailKind(parent context.Context, err error) string {

// upstreamStatusOf extracts the upstream HTTP status when the error carries
// one. The UAA token-refresh path provides it directly via
// api.ErrHTTPRequest.Status; capi CF-call errors embed it as "(status NNN)" in
// the message (parsed best-effort). When CF answered with a v3 error
// envelope there is no "(status NNN)", so the status comes from the capi
// sentinel when that names exactly one (see exactCapiStatus). Returns 0 when
// no status is recoverable, in which case it is omitted from the response.
// api.ErrHTTPRequest.Status, and capi CF-call errors via *capi.StatusError,
// envelope responses included. An error that lost the wrap chain falls
// back to the "(status NNN)" capi embeds in non-envelope messages (parsed
// best-effort), then to the capi sentinel when that names exactly one (see
// exactCapiStatus). Returns 0 when no status is recoverable, in which case
// it is omitted from the response.
func upstreamStatusOf(err error) int {
if err == nil {
return 0
Expand All @@ -188,6 +189,10 @@ func upstreamStatusOf(err error) int {
if errors.As(err, &httpReq) {
return httpReq.Status
}
var statusErr *capi.StatusError
if errors.As(err, &statusErr) {
return statusErr.StatusCode
}
if m := capiStatusRe.FindStringSubmatch(err.Error()); m != nil {
if n, convErr := strconv.Atoi(m[1]); convErr == nil {
return n
Expand Down
4 changes: 4 additions & 0 deletions src/jetstream/plugins/cloudfoundry/native_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func TestUpstreamStatusOf(t *testing.T) {
{"capi envelope 429", cfEnvelopeErr(capi.ErrRateLimited, 10013, "CF-RateLimitExceeded"), 429},
{"capi envelope wrapped", fmt.Errorf("listing apps: %w", cfEnvelopeErr(capi.ErrNotFound, 10010, "CF-ResourceNotFound")), 404},
{"capi envelope from MapHTTPError", capi.MapHTTPError(422, []byte(`{"errors":[{"code":10008,"title":"CF-UnprocessableEntity","detail":"x"}]}`)), 422},
// MapHTTPError's *capi.StatusError keeps the exact status even
// where the sentinel covers a range.
{"capi envelope 400 from MapHTTPError", capi.MapHTTPError(400, []byte(`{"errors":[{"code":10005,"title":"CF-BadQueryParameter","detail":"x"}]}`)), 400},
{"capi envelope 503 from MapHTTPError", fmt.Errorf("listing apps: %w", capi.MapHTTPError(503, []byte(`{"errors":[{"code":10001,"title":"CF-ServerError","detail":"x"}]}`))), 503},
// ErrBadRequest and ErrServerError each cover a range of statuses;
// without the exact code there is nothing to report.
{"capi envelope other 4xx", cfEnvelopeErr(capi.ErrBadRequest, 10005, "CF-BadQueryParameter"), 0},
Expand Down
Loading