2 * Copyright (c) 2024 PANTHEON.tech s.r.o. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.transport.http;
10 import static io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames.SCHEME;
11 import static io.netty.handler.codec.http2.HttpConversionUtil.ExtensionHeaderNames.STREAM_ID;
13 import com.google.common.util.concurrent.FutureCallback;
14 import io.netty.channel.Channel;
15 import io.netty.channel.ChannelHandlerContext;
16 import io.netty.handler.codec.http.FullHttpRequest;
17 import io.netty.handler.codec.http.FullHttpResponse;
18 import io.netty.handler.codec.http.HttpScheme;
19 import io.netty.handler.ssl.SslHandler;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.atomic.AtomicInteger;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
27 * Client side {@link RequestDispatcher} implementation for HTTP 2.
29 * <p>Serves as gateway to Netty {@link Channel}, performs sending requests to server, returns server responses
30 * associated. Uses request to response mapping by stream identifier.
32 final class ClientHttp2RequestDispatcher extends ClientRequestDispatcher {
33 private static final Logger LOG = LoggerFactory.getLogger(ClientHttp2RequestDispatcher.class);
35 // TODO: we access the queue only from Netty callbacks: can we use a plain HashMap?
36 private final Map<Integer, FutureCallback<FullHttpResponse>> map = new ConcurrentHashMap<>();
37 // identifier for streams initiated from client require to be odd-numbered, 1 is reserved
38 // see https://datatracker.ietf.org/doc/html/rfc7540#section-5.1.1
39 private final AtomicInteger streamIdCounter = new AtomicInteger(3);
41 private boolean ssl = false;
44 public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
45 ssl = ctx.pipeline().get(SslHandler.class) != null;
46 super.handlerAdded(ctx);
50 public void dispatch(final Channel channel, final FullHttpRequest request,
51 final FutureCallback<FullHttpResponse> callback) {
52 final var streamId = nextStreamId();
54 .setInt(STREAM_ID.text(), streamId)
55 .set(SCHEME.text(), ssl ? HttpScheme.HTTPS.name() : HttpScheme.HTTP.name());
57 // Map has to be populated first, simply because a response may arrive sooner than the successful callback
58 map.put(streamId, callback);
59 channel.writeAndFlush(request).addListener(sent -> {
60 final var cause = sent.cause();
61 if (cause != null && map.remove(streamId, callback)) {
62 callback.onFailure(cause);
68 return streamIdCounter.getAndAdd(2);
72 protected void channelRead0(final ChannelHandlerContext ctx, final FullHttpResponse response) {
73 final var streamId = response.headers().getInt(STREAM_ID.text());
74 if (streamId == null) {
75 LOG.warn("Unexpected response with no stream ID -- Dropping response object {}", response);
78 final var callback = map.remove(streamId);
79 if (callback != null) {
80 callback.onSuccess(response);
82 LOG.warn("Unexpected response with unknown or expired stream ID {} -- Dropping response object {}",