rpc_dart_http 0.2.4 copy "rpc_dart_http: ^0.2.4" to clipboard
rpc_dart_http: ^0.2.4 copied to clipboard

HTTP/1.1 caller/responder transports for rpc_dart (unary only).

example/rpc_dart_http_example.dart

// SPDX-FileCopyrightText: 2026 Karim "nogipx" Mamatkazin <nogipx@gmail.com>
//
// SPDX-License-Identifier: MIT

import 'package:http/http.dart' as http;
import 'package:rpc_dart/rpc_dart.dart';
import 'package:rpc_dart_http/rpc_dart_http.dart';
import 'package:shelf/shelf.dart' show Response;
import 'package:shelf/shelf_io.dart' as shelf_io;

// ---------------------------------------------------------------------------
// Example: rpc_dart_http — HTTP/1.1 unary transport
//
// Uses package:http (client) and package:shelf (server) — compiles to all
// platforms including JS/Wasm.
//
// Shows:
//   1. Basic server + client setup
//   2. Security policy (concurrent-stream limit, body-size limit)
//   3. Content-Type validation (415 for non-gRPC requests)
//   4. Body-read timeout (408 when the client is too slow)
//   5. CORS policy (preflight + response headers)
//   6. Custom HTTP client (e.g. for TLS / self-signed certs on native)
//   7. HTTP → gRPC error mapping (non-200 responses become gRPC errors)
// ---------------------------------------------------------------------------

void main() async {
  await example1BasicSetup();
  await example2SecurityPolicy();
  await example3Cors();
  await example4CustomHttpClient();
  await example5HttpErrorMapping();
}

// ---------------------------------------------------------------------------
// 1. Basic setup
// ---------------------------------------------------------------------------
Future<void> example1BasicSetup() async {
  print('\n=== 1. Basic setup ===');

  final serverTransport = RpcHttpResponderTransport();
  final server = await shelf_io.serve(serverTransport.handler, '127.0.0.1', 0);

  final serverEndpoint = RpcResponderEndpoint(
    transport: serverTransport,
    debugLabel: 'Server',
  );
  serverEndpoint.registerServiceContract(EchoResponder());
  serverEndpoint.start();

  final clientTransport = RpcHttpCallerTransport(
    baseUrl: 'http://127.0.0.1:${server.port}',
  );
  final clientEndpoint = RpcCallerEndpoint(
    transport: clientTransport,
    debugLabel: 'Client',
  );

  final echo = EchoCaller(clientEndpoint);
  final result = await echo.echo('Hello HTTP/1.1'.rpc);
  print('Response: ${result.value}');

  await clientEndpoint.close();
  await serverEndpoint.close();
  await server.close(force: true);
}

// ---------------------------------------------------------------------------
// 2. Security policy
// ---------------------------------------------------------------------------
Future<void> example2SecurityPolicy() async {
  print('\n=== 2. Security policy ===');

  final serverTransport = RpcHttpResponderTransport(
    securityPolicy: RpcSecurityPolicy(
      maxActiveStreams: 100,
      maxMessageLengthBytes: 4 * 1024 * 1024,
    ),
    bodyReadTimeout: const Duration(seconds: 10),
  );
  final server = await shelf_io.serve(serverTransport.handler, '127.0.0.1', 0);

  final serverEndpoint = RpcResponderEndpoint(transport: serverTransport);
  serverEndpoint.registerServiceContract(EchoResponder());
  serverEndpoint.start();

  final clientTransport = RpcHttpCallerTransport(
    baseUrl: 'http://127.0.0.1:${server.port}',
  );
  final clientEndpoint = RpcCallerEndpoint(transport: clientTransport);

  final echo = EchoCaller(clientEndpoint);
  final result = await echo.echo('secure call'.rpc);
  print('Response: ${result.value}');

  // A non-gRPC content-type is rejected with HTTP 415.
  final rawResponse = await http.post(
    Uri.parse('http://127.0.0.1:${server.port}/Echo/Echo'),
    headers: {'content-type': 'application/json'},
  );
  print('Wrong content-type → HTTP ${rawResponse.statusCode}'); // 415

  await clientEndpoint.close();
  await serverEndpoint.close();
  await server.close(force: true);
}

// ---------------------------------------------------------------------------
// 3. CORS policy
// ---------------------------------------------------------------------------
Future<void> example3Cors() async {
  print('\n=== 3. CORS ===');

  final serverTransport = RpcHttpResponderTransport(
    corsPolicy: RpcHttpCorsPolicy(
      allowedOrigins: ['https://my-app.example.com'],
      allowedHeaders: [
        'content-type',
        'authorization',
        'x-requested-with',
        'x-tenant-id',
      ],
      allowCredentials: true,
      preflightMaxAge: const Duration(minutes: 10),
    ),
  );
  final server = await shelf_io.serve(serverTransport.handler, '127.0.0.1', 0);

  final serverEndpoint = RpcResponderEndpoint(transport: serverTransport);
  serverEndpoint.registerServiceContract(EchoResponder());
  serverEndpoint.start();

  // Simulate a browser sending an OPTIONS preflight.
  final preflightReq = http.Request(
    'OPTIONS',
    Uri.parse('http://127.0.0.1:${server.port}/Echo/Echo'),
  );
  preflightReq.headers['origin'] = 'https://my-app.example.com';
  preflightReq.headers['access-control-request-method'] = 'POST';
  final preflightRes = await http.Client().send(preflightReq);
  print('Preflight → ${preflightRes.statusCode}'); // 204
  print('Allow-Origin: ${preflightRes.headers['access-control-allow-origin']}');

  await serverEndpoint.close();
  await server.close(force: true);
}

// ---------------------------------------------------------------------------
// 4. Custom HTTP client (e.g. for TLS / self-signed certs on native)
// ---------------------------------------------------------------------------
Future<void> example4CustomHttpClient() async {
  print('\n=== 4. Custom HTTP client ===');

  // On native platforms, wrap dart:io HttpClient with IOClient for full
  // TLS control (custom CA, self-signed cert acceptance, etc.):
  //
  // import 'dart:io';
  // import 'package:http/io_client.dart';
  //
  // final ioClient = HttpClient()
  //   ..badCertificateCallback = (cert, host, port) => true; // dev only
  // final client = IOClient(ioClient);
  //
  // final transport = RpcHttpCallerTransport(
  //   baseUrl: 'https://...',
  //   httpClient: client,
  // );

  // For this example, just check health with a plain client.
  final clientTransport = RpcHttpCallerTransport(
    baseUrl: 'http://127.0.0.1:8765',
    httpClient: http.Client(),
  );

  final health = await clientTransport.health();
  print('Health: ${health.level}');

  await clientTransport.close();
}

// ---------------------------------------------------------------------------
// 5. HTTP → gRPC error mapping
// ---------------------------------------------------------------------------
Future<void> example5HttpErrorMapping() async {
  print('\n=== 5. HTTP → gRPC error mapping ===');

  // A shelf handler that always returns 404.
  final server = await shelf_io.serve(
    (_) async => Response.notFound(''),
    '127.0.0.1',
    0,
  );

  final clientTransport = RpcHttpCallerTransport(
    baseUrl: 'http://127.0.0.1:${server.port}',
  );
  final clientEndpoint = RpcCallerEndpoint(transport: clientTransport);
  final echo = EchoCaller(clientEndpoint);

  try {
    await echo.echo('will fail'.rpc);
  } catch (e) {
    // gRPC error 12 = UNIMPLEMENTED (mapped from HTTP 404).
    print('Caught: $e');
  }

  // Mapping table (HTTP → gRPC status):
  //   400 → INVALID_ARGUMENT (3)
  //   401 → UNAUTHENTICATED (16)
  //   403 → PERMISSION_DENIED (7)
  //   404 → UNIMPLEMENTED (12)
  //   429 → RESOURCE_EXHAUSTED (8)
  //   499 → CANCELLED (1)
  //   500 → INTERNAL (13)
  //   502 → UNAVAILABLE (14)
  //   503 → UNAVAILABLE (14)
  //   504 → DEADLINE_EXCEEDED (4)

  await clientEndpoint.close();
  await server.close(force: true);
}

// ---------------------------------------------------------------------------
// Contract definition
// ---------------------------------------------------------------------------

abstract interface class IEchoContract implements IRpcContract {
  Future<RpcString> echo(RpcString message);
}

final class EchoResponder extends RpcResponderContract
    implements IEchoContract {
  EchoResponder() : super('Echo');

  @override
  void setup() {
    addUnaryMethod<RpcString, RpcString>(
      methodName: 'Echo',
      handler: echo,
      requestCodec: RpcString.codec,
      responseCodec: RpcString.codec,
    );
  }

  @override
  Future<RpcString> echo(RpcString message, {RpcContext? context}) async {
    return 'Echo: ${message.value}'.rpc;
  }
}

final class EchoCaller extends RpcCallerContract implements IEchoContract {
  EchoCaller(RpcCallerEndpoint endpoint) : super('Echo', endpoint);

  @override
  Future<RpcString> echo(RpcString message, {RpcContext? context}) {
    return callUnary<RpcString, RpcString>(
      methodName: 'Echo',
      requestCodec: RpcString.codec,
      responseCodec: RpcString.codec,
      request: message,
      context: context,
    );
  }
}
0
likes
140
points
74
downloads

Documentation

API reference

Publisher

verified publisherrpc.nogipx.dev

Weekly Downloads

HTTP/1.1 caller/responder transports for rpc_dart (unary only).

Homepage
Repository (GitHub)
View/report issues

Topics

#rpc #http

License

MIT (license)

Dependencies

http, rpc_dart, shelf

More

Packages that depend on rpc_dart_http