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