Skip to content

Commit b625430

Browse files
authored
Fix gRPC Web trailers encoding (#1582)
Motivation: In gRPC Web HTTP trailers are encoded as a 'regular' gRPC message; that is a length prefixed message. For grpc-web we sent trailers back as regular trailers. Modifications: - Send trailers back as a length prefixed body part. - Update tests. Result: Resolves #1580
1 parent 03b3f1f commit b625430

File tree

4 files changed

+60
-26
lines changed

4 files changed

+60
-26
lines changed

Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,17 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.State {
453453
)
454454
)
455455
} else {
456-
// No response buffer; plain gRPC Web.
457-
let trailers = HTTPHeaders(hpackHeaders: trailers)
458-
return .write(.init(part: .end(trailers), promise: promise, closeChannel: closeChannel))
456+
// No response buffer; plain gRPC Web. Trailers are encoded into the body as a regular
457+
// length-prefixed message.
458+
let buffer = GRPCWebToHTTP2ServerCodec.formatTrailers(trailers, allocator: allocator)
459+
return .write(
460+
.init(
461+
part: .body(.byteBuffer(buffer)),
462+
additionalPart: .end(nil),
463+
promise: promise,
464+
closeChannel: closeChannel
465+
)
466+
)
459467
}
460468
}
461469

@@ -671,18 +679,27 @@ extension GRPCWebToHTTP2ServerCodec {
671679
allocator: ByteBufferAllocator
672680
) -> ByteBuffer {
673681
// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
674-
let encodedTrailers = trailers.map { name, value, _ in
675-
"\(name): \(value)"
676-
}.joined(separator: "\r\n")
682+
let length = trailers.reduce(0) { partial, trailer in
683+
// +4 for: ":", " ", "\r", "\n"
684+
return partial + trailer.name.utf8.count + trailer.value.utf8.count + 4
685+
}
686+
var buffer = allocator.buffer(capacity: 5 + length)
677687

678-
var buffer = allocator.buffer(capacity: 5 + encodedTrailers.utf8.count)
679688
// Uncompressed trailer byte.
680689
buffer.writeInteger(UInt8(0x80))
681690
// Length.
682-
buffer.writeInteger(UInt32(encodedTrailers.utf8.count))
683-
// Uncompressed trailers.
684-
buffer.writeString(encodedTrailers)
691+
let lengthIndex = buffer.writerIndex
692+
buffer.writeInteger(UInt32(0))
693+
694+
var bytesWritten = 0
695+
for (name, value, _) in trailers {
696+
bytesWritten += buffer.writeString(name)
697+
bytesWritten += buffer.writeString(": ")
698+
bytesWritten += buffer.writeString(value)
699+
bytesWritten += buffer.writeString("\r\n")
700+
}
685701

702+
buffer.setInteger(UInt32(bytesWritten), at: lengthIndex)
686703
return buffer
687704
}
688705

Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ import XCTest
2424

2525
class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase {
2626
private func writeTrailers(_ trailers: HPACKHeaders, into buffer: inout ByteBuffer) {
27-
let encoded = trailers.map { "\($0.name): \($0.value)" }.joined(separator: "\r\n")
2827
buffer.writeInteger(UInt8(0x80))
29-
buffer.writeInteger(UInt32(encoded.utf8.count))
30-
buffer.writeString(encoded)
28+
try! buffer.writeLengthPrefixed(as: UInt32.self) {
29+
var length = 0
30+
for (name, value, _) in trailers {
31+
length += $0.writeString("\(name): \(value)\r\n")
32+
}
33+
return length
34+
}
3135
}
3236

3337
private func receiveHead(
@@ -106,7 +110,7 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase {
106110
on channel: EmbeddedChannel,
107111
expectedBytes: ByteBuffer? = nil
108112
) throws {
109-
let headers: HPACKHeaders = ["grpc-status": "\(status)"]
113+
let headers: HPACKHeaders = ["grpc-status": "\(status.rawValue)"]
110114
let headersPayload: HTTP2Frame.FramePayload = .headers(.init(headers: headers, endStream: true))
111115
assertThat(try channel.writeOutbound(headersPayload), .doesNotThrow())
112116

@@ -128,7 +132,10 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase {
128132
// Outbound
129133
try self.sendResponseHeaders(on: channel)
130134
try self.sendBytes([1, 2, 3], on: channel, expectedBytes: [1, 2, 3])
131-
try self.sendEnd(status: .ok, on: channel)
135+
136+
var buffer = ByteBuffer()
137+
self.writeTrailers(["grpc-status": "0"], into: &buffer)
138+
try self.sendEnd(status: .ok, on: channel, expectedBytes: buffer)
132139
}
133140

134141
func testWebTextHappyPath() throws {
@@ -150,7 +157,7 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase {
150157
// Build up the expected response, i.e. the response bytes and the trailers, base64 encoded.
151158
var expectedBodyBuffer = ByteBuffer(bytes: [1, 2, 3])
152159
let status = GRPCStatus.Code.ok
153-
self.writeTrailers(["grpc-status": "\(status)"], into: &expectedBodyBuffer)
160+
self.writeTrailers(["grpc-status": "\(status.rawValue)"], into: &expectedBodyBuffer)
154161
try self.sendEnd(status: status, on: channel, expectedBytes: expectedBodyBuffer.base64Encoded())
155162
}
156163

Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,11 @@ final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase {
223223
promise: nil,
224224
allocator: self.allocator
225225
).assertWrite { write in
226-
write.part.assertEnd {
227-
$0.assertSome { trailers in
228-
XCTAssertEqual(trailers[canonicalForm: "grpc-status"], ["0"])
229-
}
226+
write.part.assertBody { buffer in
227+
var buffer = buffer
228+
let trailers = buffer.readLengthPrefixedMessage().map { String(buffer: $0) }
229+
XCTAssertEqual(trailers, "grpc-status: 0\r\n")
230230
}
231-
232231
XCTAssertEqual(write.closeChannel, expectChannelClose)
233232
}
234233
}
@@ -330,15 +329,15 @@ final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase {
330329
write.part.assertBody { buffer in
331330
var buffer = buffer
332331
let base64Encoded = buffer.readString(length: buffer.readableBytes)!
333-
XCTAssertEqual(base64Encoded, "aGVsbG8sIHdvcmxkIYAAAAAOZ3JwYy1zdGF0dXM6IDA=")
332+
XCTAssertEqual(base64Encoded, "aGVsbG8sIHdvcmxkIYAAAAAQZ3JwYy1zdGF0dXM6IDANCg==")
334333

335334
let data = Data(base64Encoded: base64Encoded)!
336335
buffer.writeData(data)
337336

338337
XCTAssertEqual(buffer.readString(length: 13), "hello, world!")
339338
XCTAssertEqual(buffer.readInteger(), UInt8(0x80))
340-
XCTAssertEqual(buffer.readInteger(), UInt32(14))
341-
XCTAssertEqual(buffer.readString(length: 14), "grpc-status: 0")
339+
XCTAssertEqual(buffer.readInteger(), UInt32(16))
340+
XCTAssertEqual(buffer.readString(length: 16), "grpc-status: 0\r\n")
342341
XCTAssertEqual(buffer.readableBytes, 0)
343342
}
344343

@@ -672,3 +671,14 @@ extension Optional {
672671
}
673672
}
674673
}
674+
675+
extension ByteBuffer {
676+
mutating func readLengthPrefixedMessage() -> ByteBuffer? {
677+
// Read off and ignore the compression byte.
678+
if self.readInteger(as: UInt8.self) == nil {
679+
return nil
680+
}
681+
682+
return self.readLengthPrefixedSlice(as: UInt32.self)
683+
}
684+
}

Tests/GRPCTests/ServerWebTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ class ServerWebTests: EchoTestCaseBase {
4141
private func gRPCWebTrailers(status: Int = 0, message: String? = nil) -> Data {
4242
var data: Data
4343
if let message = message {
44-
data = "grpc-status: \(status)\r\ngrpc-message: \(message)".data(using: .utf8)!
44+
data = "grpc-status: \(status)\r\ngrpc-message: \(message)\r\n".data(using: .utf8)!
4545
} else {
46-
data = "grpc-status: \(status)".data(using: .utf8)!
46+
data = "grpc-status: \(status)\r\n".data(using: .utf8)!
4747
}
4848

4949
// Add the gRPC prefix with the compression byte and the 4 length bytes.

0 commit comments

Comments
 (0)