1
16 package okhttp3.logging;
17
18 import java.io.EOFException;
19 import java.io.IOException;
20 import java.nio.charset.Charset;
21 import java.util.Collections;
22 import java.util.Set;
23 import java.util.TreeSet;
24 import java.util.concurrent.TimeUnit;
25 import okhttp3.Connection;
26 import okhttp3.Headers;
27 import okhttp3.Interceptor;
28 import okhttp3.MediaType;
29 import okhttp3.OkHttpClient;
30 import okhttp3.Request;
31 import okhttp3.RequestBody;
32 import okhttp3.Response;
33 import okhttp3.ResponseBody;
34 import okhttp3.internal.http.HttpHeaders;
35 import okhttp3.internal.platform.Platform;
36 import okio.Buffer;
37 import okio.BufferedSource;
38 import okio.GzipSource;
39
40 import static okhttp3.internal.platform.Platform.INFO;
41
42
49 public final class HttpLoggingInterceptor implements Interceptor {
50 private static final Charset UTF8 = Charset.forName("UTF-8");
51
52 public enum Level {
53
54 NONE,
55
65 BASIC,
66
83 HEADERS,
84
105 BODY
106 }
107
108 public interface Logger {
109 void log(String message);
110
111
112 Logger DEFAULT = message -> Platform.get().log(INFO, message, null);
113 }
114
115 public HttpLoggingInterceptor() {
116 this(Logger.DEFAULT);
117 }
118
119 public HttpLoggingInterceptor(Logger logger) {
120 this.logger = logger;
121 }
122
123 private final Logger logger;
124
125 private volatile Set<String> headersToRedact = Collections.emptySet();
126
127 public void redactHeader(String name) {
128 Set<String> newHeadersToRedact = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
129 newHeadersToRedact.addAll(headersToRedact);
130 newHeadersToRedact.add(name);
131 headersToRedact = newHeadersToRedact;
132 }
133
134 private volatile Level level = Level.NONE;
135
136
137 public HttpLoggingInterceptor setLevel(Level level) {
138 if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
139 this.level = level;
140 return this;
141 }
142
143 public Level getLevel() {
144 return level;
145 }
146
147 @Override public Response intercept(Chain chain) throws IOException {
148 Level level = this.level;
149
150 Request request = chain.request();
151 if (level == Level.NONE) {
152 return chain.proceed(request);
153 }
154
155 boolean logBody = level == Level.BODY;
156 boolean logHeaders = logBody || level == Level.HEADERS;
157
158 RequestBody requestBody = request.body();
159 boolean hasRequestBody = requestBody != null;
160
161 Connection connection = chain.connection();
162 String requestStartMessage = "--> "
163 + request.method()
164 + ' ' + request.url()
165 + (connection != null ? " " + connection.protocol() : "");
166 if (!logHeaders && hasRequestBody) {
167 requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
168 }
169 logger.log(requestStartMessage);
170
171 if (logHeaders) {
172 if (hasRequestBody) {
173
174
175 if (requestBody.contentType() != null) {
176 logger.log("Content-Type: " + requestBody.contentType());
177 }
178 if (requestBody.contentLength() != -1) {
179 logger.log("Content-Length: " + requestBody.contentLength());
180 }
181 }
182
183 Headers headers = request.headers();
184 for (int i = 0, count = headers.size(); i < count; i++) {
185 String name = headers.name(i);
186
187 if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
188 logHeader(headers, i);
189 }
190 }
191
192 if (!logBody || !hasRequestBody) {
193 logger.log("--> END " + request.method());
194 } else if (bodyHasUnknownEncoding(request.headers())) {
195 logger.log("--> END " + request.method() + " (encoded body omitted)");
196 } else if (requestBody.isDuplex()) {
197 logger.log("--> END " + request.method() + " (duplex request body omitted)");
198 } else {
199 Buffer buffer = new Buffer();
200 requestBody.writeTo(buffer);
201
202 Charset charset = UTF8;
203 MediaType contentType = requestBody.contentType();
204 if (contentType != null) {
205 charset = contentType.charset(UTF8);
206 }
207
208 logger.log("");
209 if (isPlaintext(buffer)) {
210 logger.log(buffer.readString(charset));
211 logger.log("--> END " + request.method()
212 + " (" + requestBody.contentLength() + "-byte body)");
213 } else {
214 logger.log("--> END " + request.method() + " (binary "
215 + requestBody.contentLength() + "-byte body omitted)");
216 }
217 }
218 }
219
220 long startNs = System.nanoTime();
221 Response response;
222 try {
223 response = chain.proceed(request);
224 } catch (Exception e) {
225 logger.log("<-- HTTP FAILED: " + e);
226 throw e;
227 }
228 long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
229
230 ResponseBody responseBody = response.body();
231 long contentLength = responseBody.contentLength();
232 String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
233 logger.log("<-- "
234 + response.code()
235 + (response.message().isEmpty() ? "" : ' ' + response.message())
236 + ' ' + response.request().url()
237 + " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')');
238
239 if (logHeaders) {
240 Headers headers = response.headers();
241 for (int i = 0, count = headers.size(); i < count; i++) {
242 logHeader(headers, i);
243 }
244
245 if (!logBody || !HttpHeaders.hasBody(response)) {
246 logger.log("<-- END HTTP");
247 } else if (bodyHasUnknownEncoding(response.headers())) {
248 logger.log("<-- END HTTP (encoded body omitted)");
249 } else {
250 BufferedSource source = responseBody.source();
251 source.request(Long.MAX_VALUE);
252 Buffer buffer = source.getBuffer();
253
254 Long gzippedLength = null;
255 if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
256 gzippedLength = buffer.size();
257 try (GzipSource gzippedResponseBody = new GzipSource(buffer.clone())) {
258 buffer = new Buffer();
259 buffer.writeAll(gzippedResponseBody);
260 }
261 }
262
263 Charset charset = UTF8;
264 MediaType contentType = responseBody.contentType();
265 if (contentType != null) {
266 charset = contentType.charset(UTF8);
267 }
268
269 if (!isPlaintext(buffer)) {
270 logger.log("");
271 logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
272 return response;
273 }
274
275 if (contentLength != 0) {
276 logger.log("");
277 logger.log(buffer.clone().readString(charset));
278 }
279
280 if (gzippedLength != null) {
281 logger.log("<-- END HTTP (" + buffer.size() + "-byte, "
282 + gzippedLength + "-gzipped-byte body)");
283 } else {
284 logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
285 }
286 }
287 }
288
289 return response;
290 }
291
292 private void logHeader(Headers headers, int i) {
293 String value = headersToRedact.contains(headers.name(i)) ? "██" : headers.value(i);
294 logger.log(headers.name(i) + ": " + value);
295 }
296
297
301 static boolean isPlaintext(Buffer buffer) {
302 try {
303 Buffer prefix = new Buffer();
304 long byteCount = buffer.size() < 64 ? buffer.size() : 64;
305 buffer.copyTo(prefix, 0, byteCount);
306 for (int i = 0; i < 16; i++) {
307 if (prefix.exhausted()) {
308 break;
309 }
310 int codePoint = prefix.readUtf8CodePoint();
311 if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
312 return false;
313 }
314 }
315 return true;
316 } catch (EOFException e) {
317 return false;
318 }
319 }
320
321 private static boolean bodyHasUnknownEncoding(Headers headers) {
322 String contentEncoding = headers.get("Content-Encoding");
323 return contentEncoding != null
324 && !contentEncoding.equalsIgnoreCase("identity")
325 && !contentEncoding.equalsIgnoreCase("gzip");
326 }
327 }
328