Handling gRPC-Web Requests
Some Ema APIs are not accessible via standard REST and must be called using gRPC-Web over HTTP. This guide provides step-by-step instructions for making gRPC-Web requests using curl and command-line tools.
Prerequisites
protoc(Protocol Buffer compiler)python3curl- Access to the
.protofiles defining the service (e.g.,service/auth/v1/auth.proto)
Step 1: Encode the Request Message
Use protoc to encode your request message into protobuf binary format.
# Option A: Encode from inline text
cat <<EOF | protoc -I=. --encode=auth.v1.GenerateApiKeyRequest \
service/auth/v1/auth.proto > request.bin
email: "[email protected]"
EOF
# Option B: Encode from an input file
protoc -I=. --encode=auth.v1.GenerateApiKeyRequest \
service/auth/v1/auth.proto < input.txt > request.bin
Replace auth.v1.GenerateApiKeyRequest and the proto file path with the appropriate message type and file for your target endpoint.
Step 2: Frame the Request
gRPC-Web requires a 5-byte frame header before the protobuf payload:
+---------------+-----------------------------------+
| 1 byte flag | 4 byte payload length (big endian)|
+---------------+-----------------------------------+
| <protobuf-encoded message bytes> |
+---------------------------------------------------+
- Flag byte:
0x00for uncompressed messages - Length bytes: 4-byte big-endian integer representing the payload size
Use this Python one-liner to add the frame header:
python3 -c 'import sys,struct; data=sys.stdin.buffer.read(); \
sys.stdout.buffer.write(b"\x00"+struct.pack(">I",len(data))+data)' \
< request.bin > framed_request.bin
Example: If the protobuf payload is 18 bytes, the frame header is 00 00 00 00 12 (where 0x12 = 18 in decimal).
Step 3: Send the Request
Make the HTTP request with the required gRPC-Web headers:
curl -X POST https://your-instance.ema.co/<service-path> \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/grpc-web+proto" \
-H "x-grpc-web: 1" \
-H "x-user-agent: grpc-web-javascript/0.1" \
--data-binary @framed_request.bin -v \
--output response.bin
Required Headers
| Header | Value | Description |
|---|---|---|
Authorization | Bearer <token> | Authentication token |
Content-Type | application/grpc-web+proto | gRPC-Web content type |
x-grpc-web | 1 | gRPC-Web protocol indicator |
Optional Headers
| Header | Value | Description |
|---|---|---|
x-user-agent | grpc-web-javascript/0.1 | Client user agent identifier |
Step 4: Strip the Response Frame
The response contains both message frames and trailer frames. Use this Python script to extract the protobuf message:
#!/usr/bin/env python3
import struct
import sys
"""
Usage:
python strip_grpc_web.py response.bin > clean_payload.bin
- response.bin: full gRPC-Web response captured from curl (--output file)
- clean_payload.bin: the extracted raw protobuf message body
"""
def extract_messages(filename: str):
with open(filename, "rb") as f:
data = f.read()
idx = 0
messages = []
while idx < len(data):
if idx + 5 > len(data):
raise ValueError("Truncated frame header")
# Read gRPC-Web frame header
flag = data[idx]
length = struct.unpack(">I", data[idx + 1 : idx + 5])[0]
idx += 5
if idx + length > len(data):
raise ValueError("Truncated frame payload")
payload = data[idx : idx + length]
idx += length
# Flag 0x80 = trailers frame (not protobuf)
if flag == 0x80:
try:
print(payload.decode("utf-8"), file=sys.stderr)
except Exception:
print(f"[Trailers: {payload!r}]", file=sys.stderr)
continue
elif flag == 0x00:
messages.append(payload)
else:
print(f"Unknown frame flag {flag:#x} (skipping)", file=sys.stderr)
return messages
def main():
if len(sys.argv) != 2:
print("Usage: python strip_grpc_web.py response.bin > clean_payload.bin")
sys.exit(1)
messages = extract_messages(sys.argv[1])
if not messages:
print("No proto messages found!", file=sys.stderr)
sys.exit(2)
# Output the first proto message to stdout
sys.stdout.buffer.write(messages[0])
if __name__ == "__main__":
main()
Save this script as strip_grpc_web.py and run:
python3 strip_grpc_web.py response.bin > clean_payload.bin
The trailer frames (printed to stderr) contain gRPC status information such as grpc-status and grpc-message.
Step 5: Decode the Response
Decode the clean protobuf payload:
protoc -I=. --decode=auth.v1.GenerateApiKeyResponse \
service/auth/v1/auth.proto < clean_payload.bin
Replace the message type and proto file path with the appropriate response type for your endpoint.
Frame Format Reference
| Flag Value | Meaning |
|---|---|
0x00 | Uncompressed message frame |
0x01 | Compressed message frame |
0x80 | Trailers frame (gRPC status) |
Complete Example
Generating an API key end-to-end:
# 1. Encode
cat <<EOF | protoc -I=. --encode=auth.v1.GenerateApiKeyRequest \
service/auth/v1/auth.proto > request.bin
email: "[email protected]"
EOF
# 2. Frame
python3 -c 'import sys,struct; data=sys.stdin.buffer.read(); \
sys.stdout.buffer.write(b"\x00"+struct.pack(">I",len(data))+data)' \
< request.bin > framed_request.bin
# 3. Send
curl -X POST https://your-instance.ema.co/auth.v1.AuthService/GenerateApiKey \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/grpc-web+proto" \
-H "x-grpc-web: 1" \
--data-binary @framed_request.bin \
--output response.bin
# 4. Strip
python3 strip_grpc_web.py response.bin > clean_payload.bin
# 5. Decode
protoc -I=. --decode=auth.v1.GenerateApiKeyResponse \
service/auth/v1/auth.proto < clean_payload.bin
gRPC-Web Endpoints in Ema
The following services use gRPC-Web:
| Service | Path Prefix |
|---|---|
| Authentication | /auth.v1.AuthService/ |
| Tenant Management | /tenant.v1.TenantService/ |
| Action Manager | /workflows.v1.ActionManager/ |
| Workflow Manager | /workflows.v1.WorkflowManager/ |
| Dashboard Service | /workflows.v1.DashboardsService/ |
| Debug Log Service | /persona.v1.DebugLogService/ |
Related
- Authentication -- API key and token generation
- Access Token for Root Tenant API Key -- First-time setup
- Workflow API -- Workflow gRPC-Web endpoints
- Dashboard RPC Calls -- Dashboard gRPC-Web endpoints