/// <summary> /// Parse a multipart/mixed response body into several responses. /// </summary> /// <param name="batchContent">The response content.</param> /// <param name="batchContentType">The response content type.</param> /// <param name="async"> /// Whether to invoke the operation asynchronously. /// </param> /// <param name="cancellationToken"> /// Optional <see cref="CancellationToken"/> to propagate notifications /// that the operation should be cancelled. /// </param> /// <returns>The parsed <see cref="Response"/>s.</returns> public static async Task <Response[]> ParseAsync( Stream batchContent, string batchContentType, bool async, CancellationToken cancellationToken) { // Get the batch boundary if (batchContentType == null || !batchContentType.StartsWith(BatchConstants.MultipartContentTypePrefix, StringComparison.Ordinal)) { throw BatchErrors.InvalidBatchContentType(batchContentType); } string batchBoundary = batchContentType.Substring(BatchConstants.MultipartContentTypePrefix.Length); // Collect the responses in a dictionary (in case the Content-ID // values come back out of order) Dictionary <int, Response> responses = new Dictionary <int, Response>(); // Read through the batch body one section at a time until the // reader returns null MultipartReader reader = new MultipartReader(batchBoundary, batchContent); for (MultipartSection section = await reader.GetNextSectionAsync(async, cancellationToken).ConfigureAwait(false); section != null; section = await reader.GetNextSectionAsync(async, cancellationToken).ConfigureAwait(false)) { // Get the Content-ID header if (!section.Headers.TryGetValue(BatchConstants.ContentIdName, out StringValues contentIdValues) || contentIdValues.Count != 1 || !int.TryParse(contentIdValues[0], out int contentId)) { // If the header wasn't found, this is a failed request // with the details being sent as the first sub-operation // so we default the Content-ID to 0 contentId = 0; } // Build a response MemoryResponse response = new MemoryResponse(); responses[contentId] = response; // We're going to read the section's response body line by line using var body = new BufferedReadStream(section.Body, BatchConstants.ResponseLineSize); // The first line is the status like "HTTP/1.1 202 Accepted" string line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); string[] status = line.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries); if (status.Length != 3) { throw BatchErrors.InvalidHttpStatusLine(line); } response.SetStatus(int.Parse(status[1], CultureInfo.InvariantCulture)); response.SetReasonPhrase(status[2]); // Continue reading headers until we reach a blank line line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); while (!string.IsNullOrEmpty(line)) { // Split the header into the name and value int splitIndex = line.IndexOf(':'); if (splitIndex <= 0) { throw BatchErrors.InvalidHttpHeaderLine(line); } var name = line.Substring(0, splitIndex); var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); response.AddHeader(name, value); line = await body.ReadLineAsync(async, cancellationToken).ConfigureAwait(false); } // Copy the rest of the body as the response content var responseContent = new MemoryStream(); if (async) { await body.CopyToAsync(responseContent).ConfigureAwait(false); } else { body.CopyTo(responseContent); } responseContent.Seek(0, SeekOrigin.Begin); response.ContentStream = responseContent; } // Collect the responses and order by Content-ID Response[] ordered = new Response[responses.Count]; for (int i = 0; i < ordered.Length; i++) { ordered[i] = responses[i]; } return(ordered); }