/*
* 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.client.http;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.linecorp.armeria.common.ClosedSessionException;
import com.linecorp.armeria.common.ContentTooLargeException;
import com.linecorp.armeria.common.http.HttpData;
import com.linecorp.armeria.common.http.HttpHeaders;
import com.linecorp.armeria.internal.http.ArmeriaHttpUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2Exception;
import io.netty.handler.codec.http2.Http2Flags;
import io.netty.handler.codec.http2.Http2FrameListener;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.Http2Stream;
final class Http2ResponseDecoder extends HttpResponseDecoder implements Http2Connection.Listener,
Http2FrameListener {
private static final Logger logger = LoggerFactory.getLogger(Http2ResponseDecoder.class);
private final Http2Connection conn;
Http2ResponseDecoder(Http2Connection conn, Channel channel) {
super(channel);
this.conn = conn;
}
@Override
public void onStreamAdded(Http2Stream stream) {}
@Override
public void onStreamActive(Http2Stream stream) {}
@Override
public void onStreamHalfClosed(Http2Stream stream) {}
@Override
public void onStreamClosed(Http2Stream stream) {}
@Override
public void onStreamRemoved(Http2Stream stream) {}
@Override
public void onGoAwaySent(int lastStreamId, long errorCode, ByteBuf debugData) {}
@Override
public void onGoAwayReceived(int lastStreamId, long errorCode, ByteBuf debugData) {}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) {
ctx.fireChannelRead(settings);
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) {}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endOfStream) throws Http2Exception {
HttpResponseWrapper res = getResponse(id(streamId), endOfStream);
if (res == null) {
throw connectionError(PROTOCOL_ERROR, "received a HEADERS frame for an unknown stream: %d",
streamId);
}
final HttpHeaders converted = ArmeriaHttpUtil.toArmeria(headers);
try {
res.scheduleTimeout(ctx);
res.write(converted);
} catch (Throwable t) {
res.close(t);
throw connectionError(INTERNAL_ERROR, t, "failed to consume a HEADERS frame");
}
if (endOfStream) {
res.close();
}
}
@Override
public void onHeadersRead(
ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
onHeadersRead(ctx, streamId, headers, padding, endOfStream);
}
@Override
public int onDataRead(
ChannelHandlerContext ctx, int streamId, ByteBuf data,
int padding, boolean endOfStream) throws Http2Exception {
final HttpResponseWrapper res = getResponse(id(streamId), endOfStream);
if (res == null) {
throw connectionError(PROTOCOL_ERROR, "received a DATA frame for an unknown stream: %d",
streamId);
}
final int dataLength = data.readableBytes();
final long maxContentLength = res.maxContentLength();
if (maxContentLength > 0 && res.writtenBytes() > maxContentLength - dataLength) {
res.close(ContentTooLargeException.get());
throw connectionError(INTERNAL_ERROR,
"content length too large: %d + %d > %d (stream: %d)",
res.writtenBytes(), dataLength, maxContentLength, streamId);
}
try {
res.write(HttpData.of(data));
} catch (Throwable t) {
res.close(t);
throw connectionError(INTERNAL_ERROR, t, "failed to consume a DATA frame");
}
if (endOfStream) {
res.close();
}
// All bytes have been processed.
return dataLength + padding;
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
final HttpResponseWrapper res = removeResponse(id(streamId));
if (res == null) {
if (conn.streamMayHaveExisted(streamId)) {
if (logger.isDebugEnabled()) {
logger.debug("{} Received a late RST_STREAM frame for a closed stream: {}",
ctx.channel(), streamId);
}
} else {
throw connectionError(PROTOCOL_ERROR,
"received a RST_STREAM frame for an unknown stream: %d", streamId);
}
return;
}
res.close(ClosedSessionException.get());
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) {}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) {}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) {}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) {}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData) {}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) {}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload) {}
private static int id(int streamId) {
return streamId - 1 >>> 1;
}
}