001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.client; 018 019import java.io.*; 020import java.nio.charset.*; 021import java.time.*; 022import java.util.*; 023import java.util.logging.*; 024 025import org.apache.juneau.rest.client.assertion.*; 026 027/** 028 * An HTTP response returned by {@link RestRequest#run()}. 029 * 030 * <p> 031 * Wraps the transport-layer {@link TransportResponse} and provides higher-level accessors. 032 * Callers <b>must</b> close this response (it implements {@link Closeable}) to release transport resources. 033 * 034 * @since 9.2.1 035 */ 036@SuppressWarnings({ 037 "resource" // response is owned and closed via RestResponse.close(); getBodyStream() transfers ownership to caller 038}) 039public final class RestResponse implements Closeable { 040 041 private final TransportResponse response; 042 private final RestClient client; 043 private final RestRequest request; 044 private final Level debugLevel; 045 private final InputStream body; 046 private byte[] cachedContent; 047 private long cachedContentLength = -1; 048 private boolean debugEmitted; 049 private Duration execTime; 050 // The X-Request-Id echoed by the server for this exchange, captured at onConnect; null when the server echoed 051 // none (or onConnect never ran on a synthetic error). Never silently falls back to the sent id. 052 private String requestId; 053 054 RestResponse(TransportResponse response, RestClient client) { 055 this(response, client, null, null, 0); 056 } 057 058 RestResponse(TransportResponse response, RestClient client, RestRequest request, Level debugLevel, int debugBodyCap) { 059 this.response = response; 060 this.client = client; 061 this.request = request; 062 this.debugLevel = debugLevel; 063 var originalBody = response.getBody(); 064 if (debugLevel == Level.FINEST && originalBody != null) 065 this.body = new BoundedCaptureInputStream(originalBody, debugBodyCap); 066 else 067 this.body = originalBody; 068 if (originalBody == null) { 069 cachedContentLength = 0; 070 } else { 071 var h = response.getFirstHeader("Content-Length"); 072 if (h != null) { 073 try { 074 cachedContentLength = Long.parseLong(h.value()); 075 } catch (NumberFormatException e) { 076 cachedContentLength = -1; 077 } 078 } 079 } 080 } 081 082 /** 083 * Returns the client that produced this response. 084 * 085 * @return The client that produced this response. Never <jk>null</jk>. 086 */ 087 public RestClient getClient() { 088 return client; 089 } 090 091 /** 092 * Returns the HTTP status code. 093 * 094 * @return The status code (e.g. {@code 200}). 095 */ 096 public int getStatusCode() { 097 return response.getStatusCode(); 098 } 099 100 /** 101 * Returns the reason phrase (e.g. {@code "OK"}), or {@code null} if absent. 102 * 103 * @return The reason phrase, possibly <jk>null</jk>. 104 */ 105 public String getReasonPhrase() { 106 return response.getReasonPhrase(); 107 } 108 109 /** 110 * Returns the {@code X-Request-Id} correlation id the server echoed for this exchange. 111 * 112 * <p> 113 * Captured at {@code onConnect} from the response header (which may differ from the sent id if the server 114 * sanitized it). Returns <jk>null</jk> when the server echoed no id, or on a synthetic no-response error where 115 * {@code onConnect} never ran — it never silently falls back to the sent id. 116 * 117 * @return The echoed correlation id, or <jk>null</jk> if none was observed. 118 * @since 10.0.0 119 */ 120 public String getRequestId() { 121 return requestId; 122 } 123 124 /** 125 * Records the correlation id echoed by the server for this exchange (auto-send interceptor internal use). 126 * 127 * @param value The echoed id. 128 */ 129 void setEchoedRequestId(String value) { 130 this.requestId = value; 131 } 132 133 /** 134 * Returns all response headers. 135 * 136 * @return An unmodifiable list. Never <jk>null</jk>. 137 */ 138 public List<TransportHeader> getHeaders() { 139 return response.getHeaders(); 140 } 141 142 /** 143 * Returns the first response header with the given name (case-insensitive), or {@code null} if absent. 144 * 145 * @param name The header name. Must not be <jk>null</jk>. 146 * @return The first matching header, or <jk>null</jk> if absent. 147 */ 148 public TransportHeader getFirstHeader(String name) { 149 return response.getFirstHeader(name); 150 } 151 152 /** 153 * Returns the response body as a UTF-8 string, or {@code null} if there is no body. 154 * 155 * @return The body as a string, possibly <jk>null</jk>. 156 * @throws IOException If an I/O error occurs reading the body. 157 */ 158 public String getBodyAsString() throws IOException { 159 var stream = this.body; 160 if (stream == null) 161 return null; 162 return new String(stream.readAllBytes(), StandardCharsets.UTF_8); 163 } 164 165 /** 166 * Returns the raw response body stream, or {@code null} if there is no body. 167 * 168 * <p> 169 * Callers must not close this stream directly — close the {@link RestResponse} instead. 170 * 171 * @return The body stream, possibly <jk>null</jk>. 172 */ 173 public InputStream getBodyStream() { 174 return body; 175 } 176 177 /** 178 * Asserts that the status code is in the 2xx range. 179 * 180 * @return This object (for chaining). 181 * @throws RestCallException If the status code is not 2xx. 182 */ 183 public RestResponse assertOk() throws RestCallException { 184 var sc = response.getStatusCode(); 185 if (sc < 200 || sc > 299) 186 throw new RestCallException(sc, "Expected 2xx status but got " + sc + " " + response.getReasonPhrase()); 187 return this; 188 } 189 190 /** 191 * Asserts that the status code equals the expected value. 192 * 193 * @param expected The expected status code. 194 * @return This object (for chaining). 195 * @throws RestCallException If the status code does not match. 196 */ 197 public RestResponse assertStatus(int expected) throws RestCallException { 198 var actual = response.getStatusCode(); 199 if (actual != expected) 200 throw new RestCallException(actual, "Expected status " + expected + " but got " + actual); 201 return this; 202 } 203 204 /** 205 * Returns a fluent body accessor for this response. 206 * 207 * @return A new body accessor. Never <jk>null</jk>. 208 */ 209 public ResponseBody body() { 210 return new ResponseBody(this); 211 } 212 213 /** 214 * Returns a fluent assertion object for this response. 215 * 216 * <p> 217 * Use this for test-style validation of the response in production or test code: 218 * <p class='bjava'> 219 * <jv>resp</jv>.assertThat() 220 * .statusCode(200) 221 * .body().contains(<js>"alice"</js>); 222 * </p> 223 * 224 * @return A new assertion object. Never <jk>null</jk>. 225 */ 226 public ResponseAssertion assertThat() { 227 return new ResponseAssertion(this); 228 } 229 230 /** 231 * Returns a fluent header accessor for the named response header. 232 * 233 * @param name The header name (case-insensitive). Must not be <jk>null</jk>. 234 * @return A new header accessor. Never <jk>null</jk>. 235 */ 236 public ResponseHeader header(String name) { 237 return new ResponseHeader(name, this); 238 } 239 240 @Override /* Closeable */ 241 public void close() throws IOException { 242 if (body instanceof BoundedCaptureInputStream bcis) { 243 bcis.drain(); 244 cachedContent = bcis.getCapturedBytes(); 245 cachedContentLength = bcis.getTotalBytesRead(); 246 } 247 if (!debugEmitted && client != null && debugLevel != null) { 248 var thrown = request != null ? request.getException() : null; 249 RestClientDebugPipeline.emit(client.debugLogger, client.debugFormatter, debugLevel, request, this, thrown); 250 debugEmitted = true; 251 } 252 response.close(); 253 } 254 255 void setExecTime(Duration value) { 256 execTime = value; 257 } 258 259 /** 260 * Returns response body bytes captured for debug logging (up to the configured body cap). 261 * 262 * @return Captured response bytes, or <jk>null</jk> if not captured. 263 */ 264 public byte[] getCachedContent() { 265 return cachedContent; 266 } 267 268 /** 269 * Returns the total response body length observed while reading/draining, or {@code -1} if unknown. 270 * 271 * @return The total body length. 272 */ 273 public long getCachedContentLength() { 274 return cachedContentLength; 275 } 276 277 Duration getExecTime() { 278 return execTime; 279 } 280 281 private static final class BoundedCaptureInputStream extends FilterInputStream { 282 private final ByteArrayOutputStream capture = new ByteArrayOutputStream(); 283 private final int cap; 284 private long totalBytesRead; 285 286 BoundedCaptureInputStream(InputStream in, int cap) { 287 super(in); 288 this.cap = cap; 289 } 290 291 byte[] getCapturedBytes() { 292 return capture.toByteArray(); 293 } 294 295 long getTotalBytesRead() { 296 return totalBytesRead; 297 } 298 299 void drain() throws IOException { 300 while (read() != -1) { 301 // consume 302 } 303 } 304 305 @Override 306 public int read() throws IOException { 307 var b = super.read(); 308 if (b == -1) 309 return -1; 310 totalBytesRead++; 311 if (capture.size() < cap) 312 capture.write(b); 313 return b; 314 } 315 316 @Override 317 public int read(byte[] b, int off, int len) throws IOException { 318 var count = super.read(b, off, len); 319 if (count <= 0) 320 return count; 321 totalBytesRead += count; 322 var remaining = cap - capture.size(); 323 if (remaining > 0) 324 capture.write(b, off, Math.min(count, remaining)); 325 return count; 326 } 327 } 328}