diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 389361a..bab68d9 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -6,28 +6,28 @@ on: pull_request: branches: '*' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up JDK 8 - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: java-version: '8' distribution: 'zulu' - - - name: Cache local Maven repository - uses: actions/cache@v2.1.6 - with: - path: ~/.m2/ - key: ${{ runner.os }}-maven-${{ hashFiles('pom.xml') }} + cache: 'maven' - name: Build with Maven run: mvn clean verify - name: Upload codecov report - uses: codecov/codecov-action@v1.5.2 + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/src/main/java/br/ufsc/bridge/soap/http/SoapHttpClient.java b/src/main/java/br/ufsc/bridge/soap/http/SoapHttpClient.java index d25e352..81be857 100644 --- a/src/main/java/br/ufsc/bridge/soap/http/SoapHttpClient.java +++ b/src/main/java/br/ufsc/bridge/soap/http/SoapHttpClient.java @@ -9,11 +9,13 @@ import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.config.SocketConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; import br.ufsc.bridge.soap.http.exception.SoapHttpConnectionException; import br.ufsc.bridge.soap.http.exception.SoapHttpResponseException; @@ -53,15 +55,18 @@ public SoapHttpClient(CloseableHttpClient httpClient) { public SoapHttpResponse request(SoapHttpRequest soapHttpRequest) throws SoapHttpResponseException, SoapHttpConnectionException { HttpRequestBase httpRequest = null; + CloseableHttpResponse response = null; ByteArrayOutputStreamNoCopy baos = null; try { - HttpResponse response = this.httpClient.execute(httpRequest = soapHttpRequest.httpRequest()); + response = this.httpClient.execute(httpRequest = soapHttpRequest.httpRequest()); int responseCode = response.getStatusLine().getStatusCode(); if (responseCode == HttpStatus.SC_INTERNAL_SERVER_ERROR) { baos = new ByteArrayOutputStreamNoCopy(response.getEntity().getContent()); throw new SoapHttpResponseException("HTTP Response code: " + responseCode + " | error: " + new String(baos.toByteArray(), "UTF-8")); } else if (responseCode != HttpStatus.SC_OK) { + // a entity precisa ser consumida, senão a conexão nunca volta para o pool + EntityUtils.consumeQuietly(response.getEntity()); throw new SoapHttpResponseException("HTTP Response code: " + responseCode); } @@ -79,6 +84,7 @@ public SoapHttpResponse request(SoapHttpRequest soapHttpRequest) throws SoapHttp throw new SoapHttpResponseException("Multipart file with invalid header", e); } finally { IOUtils.closeQuietly(baos); + IOUtils.closeQuietly(response); } } diff --git a/src/test/java/br/ufsc/bridge/soap/http/SoapHttpClientTest.java b/src/test/java/br/ufsc/bridge/soap/http/SoapHttpClientTest.java index 20d7a77..c1fb533 100644 --- a/src/test/java/br/ufsc/bridge/soap/http/SoapHttpClientTest.java +++ b/src/test/java/br/ufsc/bridge/soap/http/SoapHttpClientTest.java @@ -1,5 +1,7 @@ package br.ufsc.bridge.soap.http; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; @@ -110,6 +112,50 @@ public void responseNotOk() throws ClientProtocolException, IOException, SoapHtt } } + @Test + public void responseClosedOnSuccess() throws ClientProtocolException, IOException, SoapHttpResponseException, SoapHttpConnectionException { + this.soapClient.request(new SoapHttpRequest(URL, null, this.bodyRequest)); + + verify(this.response).close(); + } + + @Test + public void responseClosedOnServerError() throws ClientProtocolException, IOException, SoapHttpConnectionException { + when(this.status.getStatusCode()).thenReturn(500); + + try { + this.soapClient.request(new SoapHttpRequest(URL, null, this.bodyRequest)); + Assert.fail("SoapHttpResponseException esperada"); + } catch (SoapHttpResponseException e) { + // esperado + } + + verify(this.response).close(); + } + + /** + * Sem consumir a entity e fechar o response, a conexão nunca volta para o pool + * e uma janela de instabilidade do servidor esgota o PoolingHttpClientConnectionManager. + */ + @Test + public void responseClosedAndEntityConsumedOnOtherErrors() throws ClientProtocolException, IOException, SoapHttpConnectionException { + when(this.entity.isStreaming()).thenReturn(true); + + for (int statusCode : new int[] { 401, 403, 404, 429, 502, 503, 504 }) { + when(this.status.getStatusCode()).thenReturn(statusCode); + + try { + this.soapClient.request(new SoapHttpRequest(URL, null, this.bodyRequest)); + Assert.fail("SoapHttpResponseException esperada para o status " + statusCode); + } catch (SoapHttpResponseException e) { + Assert.assertEquals("HTTP Response code: " + statusCode, e.getMessage()); + } + } + + verify(this.entity, times(7)).getContent(); + verify(this.response, times(7)).close(); + } + @Test @SuppressWarnings("unchecked") public void ioExcpetion() throws ClientProtocolException, IOException, SoapHttpResponseException {