// 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."); } } }