diff --git a/nats/src/nats/js/client.py b/nats/src/nats/js/client.py index d516a6e2..43936e56 100644 --- a/nats/src/nats/js/client.py +++ b/nats/src/nats/js/client.py @@ -165,8 +165,13 @@ async def _handle_async_reply(self, msg: Msg) -> None: try: resp = json.loads(msg.data) if "error" in resp: - err = nats.js.errors.APIError.from_error(resp["error"]) - future.set_exception(err) + # APIError.from_error raises rather than returning, so capture + # the constructed error and attach it to the future instead of + # letting it escape and leave the future pending forever. + try: + raise nats.js.errors.APIError.from_error(resp["error"]) + except nats.js.errors.APIError as err: + future.set_exception(err) return ack = api.PubAck.from_response(resp) diff --git a/nats/tests/test_js.py b/nats/tests/test_js.py index fd5bc1ce..31325499 100644 --- a/nats/tests/test_js.py +++ b/nats/tests/test_js.py @@ -193,6 +193,23 @@ async def test_publish_async(self): await nc.close() + @async_test + async def test_publish_async_error_ack_is_raised(self): + # Regression for #985: a server error ack must be raised on the + # returned future rather than being swallowed (logged and left + # pending forever). + nc = NATS() + await nc.connect() + js = nc.jetstream() + await js.add_stream(name="AERR", subjects=["aerr"]) + + # Expected-stream mismatch forces the server to return an error ack. + future = await js.publish_async("aerr", b"data", stream="WRONGSTREAM") + with pytest.raises(nats.js.errors.APIError): + await asyncio.wait_for(future, timeout=2) + + await nc.close() + @async_test async def test_publish_msg_ttl(self): """Test per-message TTL feature (requires NATS Server 2.11+)"""