{"record":{"id":"3d4f88b063d13e82","repo":"yudaocode/SpringBoot-Labs","slug":"error-3d4f88","errorCode":null,"errorMessage":"扣除库存失败","messagePattern":"扣除库存失败","errorType":"http","errorClass":"java.lang.RuntimeException","httpStatus":500,"severity":"error","filePath":"lab-52/lab-52-seata-at-httpclient-demo/lab-52-seata-at-httpclient-demo-order-service/src/main/java/cn/iocoder/springboot/lab52/orderservice/service/OrderServiceImpl.java","lineNumber":58,"sourceCode":"        OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);\n        orderDao.saveOrder(order);\n        logger.info(\"[createOrder] 保存订单: {}\", order.getId());\n\n        // 返回订单编号\n        return order.getId();\n    }\n\n    private void reduceStock(Long productId, Integer amount) throws IOException {\n        // 参数拼接\n        JSONObject params = new JSONObject().fluentPut(\"productId\", String.valueOf(productId))\n                .fluentPut(\"amount\", String.valueOf(amount));\n        // 执行调用\n        HttpResponse response = DefaultHttpExecutor.getInstance().executePost(\"http://127.0.0.1:8082\", \"/product/reduce-stock\",\n                params, HttpResponse.class);\n        // 解析结果\n        Boolean success = Boolean.valueOf(EntityUtils.toString(response.getEntity()));\n        if (!success) {\n            throw new RuntimeException(\"扣除库存失败\");\n        }\n    }\n\n    private void reduceBalance(Long userId, Integer price) throws IOException {\n        // 参数拼接\n        JSONObject params = new JSONObject().fluentPut(\"userId\", String.valueOf(userId))\n                .fluentPut(\"price\", String.valueOf(price));\n        // 执行调用\n        HttpResponse response = DefaultHttpExecutor.getInstance().executePost(\"http://127.0.0.1:8083\", \"/account/reduce-balance\",\n                params, HttpResponse.class);\n        // 解析结果\n        Boolean success = Boolean.valueOf(EntityUtils.toString(response.getEntity()));\n        if (!success) {\n            throw new RuntimeException(\"扣除余额失败\");\n        }\n    }\n\n}","sourceCodeStart":40,"sourceCodeEnd":76,"githubUrl":"https://github.com/yudaocode/SpringBoot-Labs/blob/6c12efaed06d12907a0f40dd2ad1f7020aec8798/lab-52/lab-52-seata-at-httpclient-demo/lab-52-seata-at-httpclient-demo-order-service/src/main/java/cn/iocoder/springboot/lab52/orderservice/service/OrderServiceImpl.java#L40-L76","documentation":"In the Seata AT + HttpClient demo, the order service remotely calls the product service via DefaultHttpExecutor.executePost('http://127.0.0.1:8082', '/product/reduce-stock', ...) and throws RuntimeException('扣除库存失败') when the response body does not parse to boolean true. The whole point of the lab: DefaultHttpExecutor propagates the Seata XID (RootContext.getXID()) in the request header so the HTTP call joins the same global transaction — and the thrown RuntimeException triggers rollback across services.","triggerScenarios":"Order creation flow (POST order endpoint on the order service) where the product service returns false (its own reduce-stock failed, e.g. insufficient stock per errors 16/17). Requires product service on 127.0.0.1:8082, Seata server running, and DefaultHttpExecutor registered for the propagation filter.","commonSituations":"Multi-service rollback verification. Typical breakages: the product service is not listening on 8082 (ConnectException surfaces before this branch); boolean parsing of the body fails (HTML error page yields false); or XID propagation missing because a plain HttpClient was used instead of DefaultHttpExecutor, so the remote branch never joins the global transaction and rollback does not span services.","solutions":["Check why the product service returned false — usually stock exhausted (errors 16/17); restock or lower the order amount.","Verify the product service is up on 127.0.0.1:8082 and the Seata server is started before driving orders.","Ensure DefaultHttpExecutor (with the Seata propagation interceptor) is the executor used, so the XID travels and rollback is global.","Log the raw response body on failure — parsing an error page as Boolean silently becomes 'false' and masks the real cause."],"exampleFix":"// before: bare boolean parse masks the real failure\nBoolean success = Boolean.valueOf(EntityUtils.toString(response.getEntity()));\nif (!success) throw new RuntimeException(\"扣除库存失败\");\n\n// after: keep the body for diagnostics\nString body = EntityUtils.toString(response.getEntity());\nif (!Boolean.parseBoolean(body)) {\n    throw new RuntimeException(\"扣除库存失败: product-service said: \" + body);\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight check before placing the order:\n// 1) product service reachable\n// 2) stock sufficient (read-only query) — avoids the doomed remote write\nInteger stock = productClient.getStock(productId);\nif (stock == null || stock < amount) {\n    throw new OrderRejectedException(\"INSUFFICIENT_STOCK\");\n}","typeGuard":null,"tryCatchPattern":"// Wrap the remote participant call; keep cause chain for Seata diagnosis:\ntry {\n    reduceStock(productId, amount);\n} catch (RuntimeException e) {\n    logger.warn(\"stock branch failed: {}\", e.getMessage());\n    throw new OrderFailedException(\"STOCK_FAILED\", e); // triggers global rollback\n}","preventionTips":["Ensure DefaultHttpExecutor (Seata XID propagation) is used for all cross-service HTTP calls in the transaction.","Start the Seata server and participant services (8082/8083) before driving orders.","Log the raw response body when parsing fails — 'false' often hides an HTML error page."],"tags":["seata","httpclient","xid-propagation","distributed-transaction","remote-call"],"backgroundTag":null,"analyzedSha":"6c12efaed06d12907a0f40dd2ad1f7020aec8798","analyzedAt":"2026-08-14T13:06:31.500Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}