// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Buffers;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft;
using Microsoft.VisualStudio.Threading;
using Nerdbank.Streams;
using StreamJsonRpc;
using StreamJsonRpc.Protocol;
///
/// A that sends requests and receives responses over HTTP using .
///
///
/// See the spec for JSON-RPC over HTTP here: https://www.jsonrpc.org/historical/json-rpc-over-http.html.
/// Only the POST method is supported.
///
public class HttpClientMessageHandler : IJsonRpcMessageHandler
{
#nullable enable
private static readonly ReadOnlyCollection AllowedContentTypes = new ReadOnlyCollection(new string[]
{
"application/json-rpc",
"application/json",
"application/jsonrequest",
});
///
/// The Content-Type header to use in requests.
///
private static readonly MediaTypeHeaderValue ContentTypeHeader = new MediaTypeHeaderValue(AllowedContentTypes[0]);
///
/// The Accept header to use in requests.
///
private static readonly MediaTypeWithQualityHeaderValue AcceptHeader = new MediaTypeWithQualityHeaderValue(AllowedContentTypes[0]);
private readonly HttpClient httpClient;
private readonly Uri requestUri;
private readonly AsyncQueue incomingMessages = new AsyncQueue();
///
/// Backing field for the property.
///
private TraceSource traceSource = new TraceSource(nameof(JsonRpc));
///
/// Initializes a new instance of the class
/// with the default .
///
/// The to use for transmitting JSON-RPC requests.
/// The URI to POST to where the entity will be the JSON-RPC message.
public HttpClientMessageHandler(HttpClient httpClient, Uri requestUri)
: this(httpClient, requestUri, new JsonMessageFormatter())
{
}
///
/// Initializes a new instance of the class.
///
/// The to use for transmitting JSON-RPC requests.
/// The URI to POST to where the entity will be the JSON-RPC message.
/// The message formatter.
public HttpClientMessageHandler(HttpClient httpClient, Uri requestUri, IJsonRpcMessageFormatter formatter)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.requestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri));
this.Formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
}
///
/// Event IDs raised to our .
///
public enum TraceEvent
{
///
/// An HTTP response with an error status code was received.
///
HttpErrorStatusCodeReceived,
}
///
/// Gets or sets the used to trace details about the HTTP transport operations.
///
/// The value can never be null.
/// Thrown by the setter if a null value is provided.
public TraceSource TraceSource
{
get => this.traceSource;
set
{
Requires.NotNull(value, nameof(value));
this.traceSource = value;
}
}
///
public bool CanRead => true;
///
public bool CanWrite => true;
///
public IJsonRpcMessageFormatter Formatter { get; }
///
public async ValueTask ReadAsync(CancellationToken cancellationToken)
{
var response = await this.incomingMessages.DequeueAsync(cancellationToken).ConfigureAwait(false);
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
using (var sequence = new Sequence())
{
#if NETCOREAPP2_1
int bytesRead;
do
{
var memory = sequence.GetMemory(4096);
bytesRead = await responseStream.ReadAsync(memory, cancellationToken).ConfigureAwait(false);
sequence.Advance(bytesRead);
}
while (bytesRead > 0);
#else
var buffer = ArrayPool.Shared.Rent(4096);
try
{
int bytesRead;
while (true)
{
bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
if (bytesRead == 0)
{
break;
}
var memory = sequence.GetMemory(bytesRead);
buffer.AsMemory(0, bytesRead).CopyTo(memory);
sequence.Advance(bytesRead);
}
}
finally
{
ArrayPool.Shared.Return(buffer);
}
#endif
return this.Formatter.Deserialize(sequence);
}
}
///
public async ValueTask WriteAsync(JsonRpcMessage content, CancellationToken cancellationToken)
{
// Cast here because we only support transmitting requests anyway.
var contentAsRequest = (JsonRpcRequest)content;
// The JSON-RPC over HTTP spec requires that we supply a Content-Length header, so we have to serialize up front
// in order to measure its length.
using (var sequence = new Sequence())
{
this.Formatter.Serialize(sequence, content);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, this.requestUri);
requestMessage.Headers.Accept.Add(AcceptHeader);
requestMessage.Content = new StreamContent(sequence.AsReadOnlySequence.AsStream());
requestMessage.Content.Headers.ContentType = ContentTypeHeader;
requestMessage.Content.Headers.ContentLength = sequence.Length;
var response = await this.httpClient.SendAsync(requestMessage, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
VerifyThrowStatusCode(contentAsRequest.IsResponseExpected ? HttpStatusCode.OK : HttpStatusCode.NoContent, response.StatusCode);
}
else
{
this.TraceSource.TraceEvent(TraceEventType.Error, (int)TraceEvent.HttpErrorStatusCodeReceived, "Received HTTP {0} {1} response to JSON-RPC request for method \"{2}\".", (int)response.StatusCode, response.StatusCode, contentAsRequest.Method);
}
// The response is expected to be a success code, or an error code with a content-type that we can deserialize.
if (response.IsSuccessStatusCode || (response.Content?.Headers.ContentType?.MediaType is string mediaType && AllowedContentTypes.Contains(mediaType)))
{
// Some requests don't merit response messages, such as notifications in JSON-RPC.
// Servers may communicate this with 202 or 204 HTTPS status codes in the response.
// Others may (poorly?) send a 200 response but with an empty entity.
if (response.Content?.Headers.ContentLength > 0)
{
// Make the response available for receiving.
this.incomingMessages.Enqueue(response);
}
}
else
{
// Throw an exception because of the unexpected failure from the server without a JSON-RPC message attached.
response.EnsureSuccessStatusCode();
}
}
}
private static void VerifyThrowStatusCode(HttpStatusCode expected, HttpStatusCode actual)
{
if (expected != actual)
{
throw new BadRpcHeaderException($"Expected \"{(int)expected} {expected}\" response but received \"{(int)actual} {actual}\" instead.");
}
}
}