@@ -315,8 +315,7 @@ func run() (exitCode int) {
315315 return 1
316316 }
317317 code := cli .GetExitCode ()
318- // A non-JSON error page can arrive with a 2xx status, which restish would
319- // treat as success. Force a non-zero exit so callers detect the failure.
318+ // Force a non-zero exit when a 2xx response carried an error page/body.
320319 if code == 0 && nonJSONErrorResponse {
321320 code = 1
322321 }
@@ -1425,17 +1424,14 @@ func overrideTableOutput() {
14251424 cli .AddContentType ("toon" , "" , - 1 , & dciToonContentType {})
14261425}
14271426
1428- // nonJSONErrorResponse records that the last API response was a non-JSON error
1429- // page (typically HTML) so run() can force a non-zero exit even when the HTTP
1430- // status was 2xx. Reset at the start of each run().
1427+ // nonJSONErrorResponse flags that the last API response was an error page/body
1428+ // so run() can force a non-zero exit even on a 2xx status. Reset each run().
14311429var nonJSONErrorResponse bool
14321430
1433- // dciResponseGuard wraps restish's response formatter to catch non-JSON error
1434- // pages before they are printed. The DCI API is JSON-only, but an upstream
1435- // timeout or maintenance window can surface the Cloudflare edge's HTML error
1436- // page (e.g. a 524 rendered as the DoiT maintenance page). Restish would dump
1437- // that raw HTML to stdout, which is confusing and hard to script against, so we
1438- // replace it with a clear, actionable message and a non-zero exit code.
1431+ // dciResponseGuard wraps restish's formatter to catch error responses the DCI
1432+ // API can leak as non-JSON HTML (e.g. a Cloudflare 524 maintenance page) or as
1433+ // a JSON error body under a locked 2xx status, replacing restish's success
1434+ // handling with a clear message and a non-zero exit code.
14391435type dciResponseGuard struct {
14401436 next cli.ResponseFormatter
14411437}
@@ -1446,6 +1442,15 @@ func (g dciResponseGuard) Format(resp cli.Response) error {
14461442 printNonJSONError (resp )
14471443 return nil
14481444 }
1445+ if msg , ok := jsonApplicationError (resp ); ok {
1446+ // Print the structured error body as usual, then flag a non-zero exit.
1447+ nonJSONErrorResponse = true
1448+ if err := g .next .Format (resp ); err != nil {
1449+ return err
1450+ }
1451+ fmt .Fprintf (os .Stderr , "Error: the DoiT API returned an application error: %s\n " , msg )
1452+ return nil
1453+ }
14491454 return g .next .Format (resp )
14501455}
14511456
@@ -1458,11 +1463,9 @@ func installResponseGuard() {
14581463 cli .Formatter = dciResponseGuard {next : cli .Formatter }
14591464}
14601465
1461- // isHTMLErrorPage reports whether a parsed response is an HTML page rather than
1462- // the JSON the DCI API is expected to return. It checks the Content-Type header
1463- // and, because edge error pages sometimes carry a misleading or absent type,
1464- // sniffs the body for an HTML prefix. Streaming (text/event-stream) and JSON
1465- // responses are intentionally left untouched.
1466+ // isHTMLErrorPage reports whether a response is HTML rather than JSON, checking
1467+ // the Content-Type and sniffing the body (edge error pages can carry a
1468+ // misleading or absent type). JSON and SSE responses are left untouched.
14661469func isHTMLErrorPage (resp cli.Response ) bool {
14671470 if strings .Contains (strings .ToLower (headerValue (resp .Headers , "Content-Type" )), "text/html" ) {
14681471 return true
@@ -1484,6 +1487,36 @@ func bodyLooksLikeHTML(s string) bool {
14841487 strings .HasPrefix (s , "<body" )
14851488}
14861489
1490+ // jsonApplicationError reports whether a 2xx response carries a non-empty
1491+ // top-level JSON `error` field (e.g. an AVA askSync failure under a locked 2xx
1492+ // status), returning a human-readable message. Such a body is not a valid DCI
1493+ // success shape, so it is treated as a failure.
1494+ func jsonApplicationError (resp cli.Response ) (string , bool ) {
1495+ if resp .Status < 200 || resp .Status >= 300 {
1496+ return "" , false
1497+ }
1498+ body , ok := resp .Body .(map [string ]interface {})
1499+ if ! ok {
1500+ return "" , false
1501+ }
1502+ switch v := body ["error" ].(type ) {
1503+ case string :
1504+ if strings .TrimSpace (v ) == "" {
1505+ return "" , false
1506+ }
1507+ return v , true
1508+ case map [string ]interface {}:
1509+ if len (v ) == 0 {
1510+ return "" , false
1511+ }
1512+ if m , ok := v ["message" ].(string ); ok && strings .TrimSpace (m ) != "" {
1513+ return m , true
1514+ }
1515+ return "application error" , true
1516+ }
1517+ return "" , false
1518+ }
1519+
14871520func printNonJSONError (resp cli.Response ) {
14881521 var b strings.Builder
14891522 b .WriteString ("Error: the DoiT API returned a non-JSON (HTML) response.\n " )
0 commit comments