{"record":{"id":"90159bff95c209fe","repo":"apache/shenyu","slug":"circuitbreakerstatuscodeexception-for-non-2xx-response","errorCode":null,"errorMessage":"CircuitBreakerStatusCodeException for non-2xx response status (defaults to 500 when status is null)","messagePattern":"CircuitBreakerStatusCodeException for non-2xx response status \\(defaults to 500 when status is null\\)","errorType":"http","errorClass":"CircuitBreakerStatusCodeException","httpStatus":null,"severity":"error","filePath":"shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-resilience4j/src/main/java/org/apache/shenyu/plugin/resilience4j/Resilience4JPlugin.java","lineNumber":90,"sourceCode":"            return combined(exchange, chain, rule);\n        }\n        return rateLimiter(exchange, chain, rule);\n    }\n\n    private Mono<Void> rateLimiter(final ServerWebExchange exchange, final ShenyuPluginChain chain, final RuleData rule) {\n        return ratelimiterExecutor.run(\n                chain.execute(exchange), fallback(ratelimiterExecutor, exchange, null), Resilience4JBuilder.build(rule))\n                .onErrorResume(throwable -> ratelimiterExecutor.withoutFallback(exchange, throwable));\n    }\n\n    private Mono<Void> combined(final ServerWebExchange exchange, final ShenyuPluginChain chain, final RuleData rule) {\n        Resilience4JConf conf = Resilience4JBuilder.build(rule);\n        return combinedExecutor.run(\n                chain.execute(exchange).doOnSuccess(v -> {\n                    HttpStatusCode status = exchange.getResponse().getStatusCode();\n                    if (Objects.isNull(status) || !status.is2xxSuccessful()) {\n                        exchange.getResponse().setStatusCode(null);\n                        throw new CircuitBreakerStatusCodeException(Objects.isNull(status) ? HttpStatus.INTERNAL_SERVER_ERROR : status);\n                    }\n                }), fallback(combinedExecutor, exchange, conf.getFallBackUri()), conf);\n    }\n\n    private Function<Throwable, Mono<Void>> fallback(final Executor executor,\n                                                     final ServerWebExchange exchange, final String uri) {\n        return throwable -> executor.fallback(exchange, UriUtils.createUri(uri), throwable).doFinally(monoV -> {\n            final Consumer<HttpStatusCode> consumer = exchange.getAttribute(Constants.METRICS_RESILIENCE4J);\n            Optional.ofNullable(consumer).ifPresent(c -> c.accept(exchange.getResponse().getStatusCode()));\n        });\n    }\n\n    @Override\n    public int getOrder() {\n        return PluginEnum.RESILIENCE4J.getCode();\n    }\n\n    @Override","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/apache/shenyu/blob/567142e07261b3e615ae8850b30f4421f455cc5d/shenyu-plugin/shenyu-plugin-fault-tolerance/shenyu-plugin-resilience4j/src/main/java/org/apache/shenyu/plugin/resilience4j/Resilience4JPlugin.java#L72-L108","documentation":"The resilience4j circuit-breaker plugin treats any non-2xx gateway response as a failure signal: inside doOnSuccess it throws CircuitBreakerStatusCodeException (carrying the response status, or 500 when null) and resets the status so the breaker records the error and the fallback URI can serve the response.","triggerScenarios":"A request matching a resilience4j rule completes but exchange.getResponse().getStatusCode() is not 2xxSuccessful (e.g. downstream returned 404/502), or the status is null at doOnSuccess time — thrown from combined() called by doExecute.","commonSituations":"Backend service returns 4xx/5xx for business errors and the operator expects the original status to pass through, but the fallback URI intercepts; broken downstream deployments causing 502s; misconfigured fallback URI leading to repeated breaker trips.","solutions":["Configure a fallbackUri on the resilience4j rule so non-2xx responses get a controlled fallback instead of an error page.","Fix the upstream service that is returning the non-2xx status if the error is unexpected.","If 4xx business statuses should not count as circuit failures, narrow the rule or adjust plugin behavior so only 5xx trigger the breaker.","Check why the response status was null (response committed/reset race) if the message reports the default 500."],"exampleFix":"// before (rule without fallback)\n{\"fallBackUri\": null}\n// after\n{\"fallBackUri\": \"/fallback/circuitBreaker\"}","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"return chain.execute(exchange)\n  .onErrorResume(CircuitBreakerStatusCodeException.class, e ->\n      fallbackHandler.fallback(exchange, UriUtils.createUri(fallbackUri), e));","preventionTips":["Always configure fallBackUri on resilience4j rules.","Monitor upstream 4xx/5xx rates; any non-2xx counts toward the breaker.","Decide explicitly whether business 4xx should count as failures and adjust rules accordingly."],"tags":["circuit-breaker","resilience4j","http-status"],"backgroundTag":"http-non-2xx-response","analyzedSha":"567142e07261b3e615ae8850b30f4421f455cc5d","analyzedAt":"2026-09-12T10:08:21.293Z","contentChangedAt":"2026-09-12T10:08:21.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}