gRPC-Web Client Runtime Library for browser communication with gRPC services
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Comprehensive error handling system with gRPC status codes and HTTP status mapping for robust error management in gRPC-Web applications.
gRPC-specific error class that extends the standard Error with status codes and metadata.
/**
* gRPC-Web error object containing status code, message, and metadata
*/
class RpcError extends Error {
/**
* Create a new gRPC error
* @param code - gRPC status code
* @param message - Error message
* @param metadata - Optional error metadata
*/
constructor(code: StatusCode, message: string, metadata?: Metadata);
/** gRPC status code */
code: StatusCode;
/** Error metadata from the server */
metadata: Metadata;
/**
* String representation of the error
* @returns Formatted error string with status code name
*/
toString(): string;
}Enumeration of all standard gRPC status codes with utility functions for HTTP status mapping.
/**
* gRPC Status Codes enumeration
*/
enum StatusCode {
/** Not an error; returned on success */
OK = 0,
/** The operation was cancelled (typically by the caller) */
CANCELLED = 1,
/** Unknown error */
UNKNOWN = 2,
/** Client specified an invalid argument */
INVALID_ARGUMENT = 3,
/** Deadline expired before operation could complete */
DEADLINE_EXCEEDED = 4,
/** Some requested entity was not found */
NOT_FOUND = 5,
/** Some entity that we attempted to create already exists */
ALREADY_EXISTS = 6,
/** The caller does not have permission to execute the specified operation */
PERMISSION_DENIED = 7,
/** Some resource has been exhausted */
RESOURCE_EXHAUSTED = 8,
/** Operation was rejected because the system is not in a state required for execution */
FAILED_PRECONDITION = 9,
/** The operation was aborted, typically due to a concurrency issue */
ABORTED = 10,
/** Operation was attempted past the valid range */
OUT_OF_RANGE = 11,
/** Operation is not implemented or not supported/enabled */
UNIMPLEMENTED = 12,
/** Internal errors - something is very broken */
INTERNAL = 13,
/** The service is currently unavailable */
UNAVAILABLE = 14,
/** Unrecoverable data loss or corruption */
DATA_LOSS = 15,
/** The request does not have valid authentication credentials */
UNAUTHENTICATED = 16
}
/**
* StatusCode utility functions
*/
namespace StatusCode {
/**
* Convert HTTP Status code to gRPC Status code
* @param httpStatus - HTTP status code number
* @returns Corresponding gRPC status code
*/
function fromHttpStatus(httpStatus: number): StatusCode;
/**
* Convert gRPC Status code to HTTP Status code
* @param statusCode - gRPC status code
* @returns Corresponding HTTP status code
*/
function getHttpStatus(statusCode: StatusCode): number;
/**
* Get human-readable name for a status code
* @param statusCode - gRPC status code
* @returns Human-readable status name
*/
function statusCodeName(statusCode: StatusCode): string;
}Interface representing gRPC status information included in responses and errors.
/**
* gRPC status information
*/
interface Status {
/** Status code number */
code: number;
/** Status details message */
details: string;
/** Optional metadata associated with the status */
metadata?: Metadata;
}Usage Examples:
import {
GrpcWebClientBase,
StatusCode,
RpcError
} from "grpc-web";
const client = new GrpcWebClientBase();
// Basic error handling
try {
const response = await client.unaryCall(url, request, metadata, methodDescriptor);
console.log('Success:', response);
} catch (error) {
if (error instanceof RpcError) {
console.error('gRPC Error:', error.code, error.message);
console.error('Error metadata:', error.metadata);
// Handle specific error types
switch (error.code) {
case StatusCode.NOT_FOUND:
console.error('Resource not found');
break;
case StatusCode.PERMISSION_DENIED:
console.error('Access denied');
break;
case StatusCode.UNAUTHENTICATED:
console.error('Authentication required');
break;
default:
console.error('Unexpected error:', StatusCode.statusCodeName(error.code));
}
} else {
console.error('Non-gRPC error:', error);
}
}Retry Logic with Exponential Backoff:
async function callWithRetry<T>(
callFn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
let lastError: RpcError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await callFn();
} catch (error) {
lastError = error as RpcError;
// Don't retry for these error types
if (error.code === StatusCode.INVALID_ARGUMENT ||
error.code === StatusCode.NOT_FOUND ||
error.code === StatusCode.PERMISSION_DENIED ||
error.code === StatusCode.UNAUTHENTICATED) {
throw error;
}
// Retry for transient errors
if (attempt < maxRetries && (
error.code === StatusCode.UNAVAILABLE ||
error.code === StatusCode.DEADLINE_EXCEEDED ||
error.code === StatusCode.INTERNAL)) {
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw lastError;
}
// Usage
const response = await callWithRetry(() =>
client.unaryCall(url, request, metadata, methodDescriptor)
);Error Context and Logging:
class GrpcErrorHandler {
static logError(error: RpcError, context: string): void {
const statusName = StatusCode.statusCodeName(error.code);
console.error(`[${context}] gRPC Error: ${statusName} (${error.code})`);
console.error(`Message: ${error.message}`);
if (Object.keys(error.metadata).length > 0) {
console.error('Metadata:', error.metadata);
}
// Add to monitoring/analytics
this.reportError(error, context);
}
static reportError(error: RpcError, context: string): void {
// Send to monitoring service
analytics.track('grpc_error', {
status_code: error.code,
status_name: StatusCode.statusCodeName(error.code),
message: error.message,
context: context
});
}
static isRetryableError(error: RpcError): boolean {
return error.code === StatusCode.UNAVAILABLE ||
error.code === StatusCode.DEADLINE_EXCEEDED ||
error.code === StatusCode.INTERNAL ||
error.code === StatusCode.RESOURCE_EXHAUSTED;
}
static isClientError(error: RpcError): boolean {
return error.code === StatusCode.INVALID_ARGUMENT ||
error.code === StatusCode.NOT_FOUND ||
error.code === StatusCode.ALREADY_EXISTS ||
error.code === StatusCode.PERMISSION_DENIED ||
error.code === StatusCode.UNAUTHENTICATED ||
error.code === StatusCode.FAILED_PRECONDITION ||
error.code === StatusCode.OUT_OF_RANGE ||
error.code === StatusCode.UNIMPLEMENTED;
}
}Stream Error Handling:
function handleStreamingCall<T>(
stream: ClientReadableStream<T>
): Promise<T[]> {
return new Promise((resolve, reject) => {
const results: T[] = [];
let hasErrored = false;
stream.on('data', (data) => {
if (!hasErrored) {
results.push(data);
}
});
stream.on('error', (error: RpcError) => {
hasErrored = true;
// Log error with context
GrpcErrorHandler.logError(error, 'streaming_call');
// Handle specific streaming errors
if (error.code === StatusCode.CANCELLED) {
console.log('Stream was cancelled');
resolve(results); // Return partial results
} else if (error.code === StatusCode.DEADLINE_EXCEEDED) {
console.log('Stream timeout - returning partial results');
resolve(results);
} else {
reject(error);
}
});
stream.on('end', () => {
if (!hasErrored) {
resolve(results);
}
});
// Set timeout for the stream
setTimeout(() => {
if (!hasErrored) {
stream.cancel();
reject(new RpcError(StatusCode.DEADLINE_EXCEEDED, 'Stream timeout'));
}
}, 30000);
});
}gRPC-Web provides utilities to convert between gRPC and HTTP status codes:
// Convert HTTP to gRPC status
const grpcStatus = StatusCode.fromHttpStatus(404); // StatusCode.NOT_FOUND
const grpcStatus2 = StatusCode.fromHttpStatus(500); // StatusCode.UNKNOWN
// Convert gRPC to HTTP status
const httpStatus = StatusCode.getHttpStatus(StatusCode.NOT_FOUND); // 404
const httpStatus2 = StatusCode.getHttpStatus(StatusCode.INTERNAL); // 500
// Get human-readable names
const statusName = StatusCode.statusCodeName(StatusCode.PERMISSION_DENIED); // "PERMISSION_DENIED"Authentication Errors:
try {
const response = await client.unaryCall(url, request, {}, methodDescriptor);
} catch (error) {
if (error.code === StatusCode.UNAUTHENTICATED) {
// Redirect to login or refresh token
await refreshAuthToken();
// Retry the call
return client.unaryCall(url, request, { 'authorization': getAuthToken() }, methodDescriptor);
}
}Rate Limiting:
try {
const response = await client.unaryCall(url, request, metadata, methodDescriptor);
} catch (error) {
if (error.code === StatusCode.RESOURCE_EXHAUSTED) {
// Check for retry-after in metadata
const retryAfter = error.metadata['retry-after'];
if (retryAfter) {
const delay = parseInt(retryAfter) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
// Retry the call
return client.unaryCall(url, request, metadata, methodDescriptor);
}
}
}Service Unavailable:
try {
const response = await client.unaryCall(url, request, metadata, methodDescriptor);
} catch (error) {
if (error.code === StatusCode.UNAVAILABLE) {
// Try fallback service or cached data
return getFallbackData();
}
}Servers can include additional error information in metadata:
try {
const response = await client.unaryCall(url, request, metadata, methodDescriptor);
} catch (error) {
const errorId = error.metadata['error-id'];
const errorDetails = error.metadata['error-details'];
console.error(`Error ${errorId}: ${error.message}`);
if (errorDetails) {
console.error('Details:', errorDetails);
}
// Report to support with error ID
reportToSupport(errorId, error);
}