/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.linecorp.armeria.server.http.encoding; import static java.util.Objects.requireNonNull; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.function.Predicate; import java.util.zip.DeflaterOutputStream; import javax.annotation.Nullable; import org.reactivestreams.Subscriber; import com.linecorp.armeria.common.MediaType; import com.linecorp.armeria.common.http.FilteredHttpResponse; import com.linecorp.armeria.common.http.HttpData; import com.linecorp.armeria.common.http.HttpHeaderNames; import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.http.HttpObject; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.common.http.HttpStatusClass; import com.linecorp.armeria.common.stream.FilteredStreamMessage; /** * A {@link FilteredStreamMessage} that applies HTTP encoding to {@link HttpObject}s as they are published. */ class HttpEncodedResponse extends FilteredHttpResponse { private final HttpEncodingType encodingType; private final Predicate<MediaType> encodableContentTypePredicate; private final int minBytesToForceChunkedAndEncoding; @Nullable private ByteArrayOutputStream encodedStream; @Nullable private DeflaterOutputStream encodingStream; private boolean headersSent; HttpEncodedResponse( HttpResponse delegate, HttpEncodingType encodingType, Predicate<MediaType> encodableContentTypePredicate, int minBytesToForceChunkedAndEncoding) { super(delegate); this.encodingType = requireNonNull(encodingType, "encodingType"); this.encodableContentTypePredicate = requireNonNull(encodableContentTypePredicate, "encodableContentTypePredicate"); this.minBytesToForceChunkedAndEncoding = HttpEncodingService.validateMinBytesToForceChunkedAndEncoding( minBytesToForceChunkedAndEncoding); } @Override protected HttpObject filter(HttpObject obj) { if (obj instanceof HttpHeaders) { HttpHeaders headers = (HttpHeaders) obj; // Skip informational headers. if (headers.status().codeClass() == HttpStatusClass.INFORMATIONAL) { return obj; } if (headersSent) { // Trailing headers, no modification. return obj; } headersSent = true; if (!shouldEncodeResponse(headers)) { return obj; } encodedStream = new ByteArrayOutputStream(); encodingStream = HttpEncoders.getEncodingOutputStream(encodingType, encodedStream); // Always use chunked encoding when compressing. headers.remove(HttpHeaderNames.CONTENT_LENGTH); switch (encodingType) { case GZIP: headers.set(HttpHeaderNames.CONTENT_ENCODING, "gzip"); break; case DEFLATE: headers.set(HttpHeaderNames.CONTENT_ENCODING, "deflate"); break; } headers.set(HttpHeaderNames.VARY, HttpHeaderNames.ACCEPT_ENCODING.toString()); return headers; } if (encodingStream == null) { // Encoding was disabled for this response. return obj; } HttpData data = (HttpData) obj; try { encodingStream.write(data.array(), data.offset(), data.length()); encodingStream.flush(); return HttpData.of(encodedStream.toByteArray()); } catch (IOException e) { throw new IllegalStateException( "Error encoding HttpData, this should not happen with byte arrays.", e); } finally { encodedStream.reset(); } } @Override protected void beforeComplete(Subscriber<? super HttpObject> subscriber) { closeEncoder(); if (encodedStream != null && encodedStream.size() > 0) { subscriber.onNext(HttpData.of(encodedStream.toByteArray())); } } @Override protected void beforeError(Subscriber<? super HttpObject> subscriber, Throwable cause) { closeEncoder(); } private void closeEncoder() { if (encodingStream == null) { return; } try { encodingStream.close(); } catch (IOException e) { throw new IllegalStateException( "Error closing encodingStream, this should not happen with byte arrays.", e); } } private boolean shouldEncodeResponse(HttpHeaders headers) { if (headers.contains(HttpHeaderNames.CONTENT_ENCODING)) { // We don't do automatic encoding if the user-supplied headers contain // Content-Encoding. return false; } if (headers.contains(HttpHeaderNames.CONTENT_TYPE)) { // Make sure the content type is worth encoding. try { MediaType contentType = MediaType.parse(headers.get(HttpHeaderNames.CONTENT_TYPE)); if (!encodableContentTypePredicate.test(contentType)) { return false; } } catch (IllegalArgumentException e) { // Don't know content type of response, don't encode. return false; } } if (headers.contains(HttpHeaderNames.CONTENT_LENGTH)) { // We switch to chunked encoding and compress the response if it's reasonably // large as the compression savings should outweigh the chunked encoding // overhead. if (headers.getInt(HttpHeaderNames.CONTENT_LENGTH) < minBytesToForceChunkedAndEncoding) { return false; } } return true; } }