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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.2] - 2026-09-04

### Fixed

- **A successful CX2 network update was reported as a failure.** `updateCX2Network` treated only `204 No Content` as success, but `PUT /v3/networks/{networkid}` answers `200` with an `NdexObjectUpdateStatus` body. A successful v3 update therefore took the error path, where its own success body failed to parse as an `NDExError` and surfaced as `Unrecognized field "uuid"` — on an update the server had already applied, making it a false failure a caller would retry. Any 2xx is now treated as success, so both the v2 (`204`) and v3 (`200`) contracts are honoured.
- **Update errors are read from the error stream.** The same method read the response body with `getInputStream()`, which throws for a 4xx or 5xx, so a genuine server error surfaced as an `IOException` rather than the `NdexException` the body describes. It now reads `getErrorStream()`, matching every other method in the client.

## [3.0.1] - 2026-09-03

### Added
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.ndexbio.client</groupId>
<artifactId>ndex-java-client</artifactId>
<version>3.0.1</version>
<version>3.0.2</version>
<name>NDEx Java REST Client</name>
<description>REST Client for in applications that access NDEx</description>
<inceptionYear>2013</inceptionYear>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,8 +889,16 @@ private void updateNetwork(UUID networkUUID, InputStream input, final String rou
HttpURLConnection con = ndexRestClient.createReturningConnection(route + "/" + networkUUID.toString() + query,
input, "PUT",
jsonAcceptContentRequestProperties);
if (con.getResponseCode() != HttpURLConnection.HTTP_NO_CONTENT){
ndexRestClient.processNdexSpecificException(con.getInputStream(), con.getResponseCode(), new ObjectMapper());
final int status = con.getResponseCode();
// Any 2xx is a success. The v2 endpoint answers 204 No Content, but v3 answers 200 with an
// NdexObjectUpdateStatus body -- so checking only for 204 sent a successful v3 update down the
// error path, where the success body failed to parse as an NDExError and surfaced as
// 'Unrecognized field "uuid"' on an update that had in fact already been applied.
if (status < HttpURLConnection.HTTP_OK || status >= HttpURLConnection.HTTP_MULT_CHOICE) {
// Error bodies arrive on the error stream; getInputStream() throws for 4xx and 5xx.
try (InputStream errorBody = con.getErrorStream()) {
ndexRestClient.processNdexSpecificException(errorBody, status, new ObjectMapper());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -254,6 +255,53 @@ private String captureUpdateRoute(VisibilityType visibility) throws Exception {
return route.getValue();
}

@Test
public void updateSucceedsWhenV3AnswersTwoHundredWithABody() throws Exception {
// The bug this guards: only 204 was treated as success, so a successful v3 update took the error
// path and failed parsing its own success body as an NDExError.
NdexRestClient client = mock(NdexRestClient.class);
expect(client.createReturningConnection(anyObject(String.class), anyObject(InputStream.class), eq("PUT"),
anyObject())).andReturn(okWithBodyConnection());
replay(client);

new NdexRestClientModelAccessLayer(client).updateCX2Network(NETWORK_ID, cx2Stream(),
VisibilityType.PUBLIC);

verify(client);
}

@Test
public void updateStillSucceedsWhenV2AnswersNoContent() throws Exception {
NdexRestClient client = mock(NdexRestClient.class);
expect(client.createReturningConnection(anyObject(String.class), anyObject(InputStream.class), eq("PUT"),
anyObject())).andReturn(noContentConnection());
replay(client);

new NdexRestClientModelAccessLayer(client).updateCXNetwork(NETWORK_ID, cx2Stream());

verify(client);
}

@Test
public void updateReportsAServerErrorFromTheErrorStream() throws Exception {
// getInputStream() throws for a 4xx, so the error body has to come off getErrorStream()
NdexRestClient client = mock(NdexRestClient.class);
expect(client.createReturningConnection(anyObject(String.class), anyObject(InputStream.class), eq("PUT"),
anyObject())).andReturn(notFoundConnection());
client.processNdexSpecificException(anyObject(InputStream.class), eq(HttpURLConnection.HTTP_NOT_FOUND),
anyObject(com.fasterxml.jackson.databind.ObjectMapper.class));
expectLastCall().andThrow(new org.ndexbio.model.exceptions.ObjectNotFoundException("network", NETWORK_ID.toString()));
replay(client);

try {
new NdexRestClientModelAccessLayer(client).updateCX2Network(NETWORK_ID, cx2Stream(), null);
fail("expected the server error to surface");
} catch (org.ndexbio.model.exceptions.NdexException expected) {
// the point is that it is an NdexException, not a Jackson parse failure
}
verify(client);
}

// ---------- helpers ----------

private static InputStream cx2Stream() {
Expand All @@ -269,6 +317,29 @@ private static HttpURLConnection createdConnection(UUID newId) throws Exception
return con;
}

/** What v3 actually answers to PUT /v3/networks/{id}: 200 with an NdexObjectUpdateStatus body. */
private static HttpURLConnection okWithBodyConnection() throws Exception {
HttpURLConnection con = mock(HttpURLConnection.class);
expect(con.getResponseCode()).andReturn(HttpURLConnection.HTTP_OK).anyTimes();
expect(con.getInputStream()).andReturn(new ByteArrayInputStream(
("{\"uuid\":\"" + NETWORK_ID + "\",\"modificationTime\":\"2026-09-04T00:00:00Z\"}")
.getBytes(StandardCharsets.UTF_8))).anyTimes();
expect(con.getErrorStream()).andReturn(null).anyTimes();
replay(con);
return con;
}

/** A failing update: an NDExError body on the error stream. */
private static HttpURLConnection notFoundConnection() throws Exception {
HttpURLConnection con = mock(HttpURLConnection.class);
expect(con.getResponseCode()).andReturn(HttpURLConnection.HTTP_NOT_FOUND).anyTimes();
expect(con.getErrorStream()).andReturn(new ByteArrayInputStream(
"{\"errorCode\":\"NDEx_Object_Not_Found\",\"message\":\"no such network\"}"
.getBytes(StandardCharsets.UTF_8))).anyTimes();
replay(con);
return con;
}

private static HttpURLConnection noContentConnection() throws Exception {
HttpURLConnection con = mock(HttpURLConnection.class);
expect(con.getResponseCode()).andReturn(HttpURLConnection.HTTP_NO_CONTENT).anyTimes();
Expand Down
Loading