handle method

Handles one REST request (what the in-process transport and the bound server call). Throws NetworkException while networkDown.

Implementation

Future<FreedomeResponse> handle(FreedomeRequest r) async {
  if (networkDown) throw const NetworkException('fake: network down');
  requests.add(r);
  if (latency > Duration.zero) await Future<void>.delayed(latency);
  if (r.path == '/graphql') return graphql.handleHttp(r);
  ApiOperation? op;
  Map<String, String>? params;
  final allowed = <String>{};
  for (final o in Operations.all) {
    final m = o.match(r.method, r.path);
    if (m != null) {
      op = o;
      params = m;
      break;
    }
    if (o.match(o.method, r.path) != null) allowed.add(o.method);
  }
  if (op == null) {
    if (allowed.isNotEmpty) {
      return FreedomeResponse.json(
        405,
        _problemJson(
          405,
          'method-not-allowed',
          '${r.method} not allowed on ${r.path}',
        ),
        headers: {..._problemCt, 'allow': allowed.join(', ')},
      );
    }
    return _problem(404, 'not-found', 'no route ${r.method} ${r.path}');
  }
  bind_impl.recordExercised('Operations.${op.id}');
  final fail = _failNext[op.id];
  if (fail != null) {
    if (fail.$3 <= 1) {
      _failNext.remove(op.id);
    } else {
      _failNext[op.id] = (fail.$1, fail.$2, fail.$3 - 1);
    }
    return FreedomeResponse.json(
      fail.$2,
      fail.$1.toJson(),
      headers: _problemCt,
    );
  }
  final rl = _rateLimit[op.id];
  if (rl != null) {
    if (rl.$1 <= 1) {
      _rateLimit.remove(op.id);
    } else {
      _rateLimit[op.id] = (rl.$1 - 1, rl.$2);
    }
    return FreedomeResponse.json(
      429,
      _problemJson(429, 'rate-limited', 'slow down'),
      headers: {..._problemCt, 'retry-after': '${rl.$2.inSeconds}'},
    );
  }
  final principal = _principal(r.headers['authorization']);
  if (op.scope != 'anon') {
    if (principal == null) {
      return _problem(401, 'unauthenticated', 'credentials required');
    }
    if (op.scope != 'any' && !principal.scopes.contains(op.scope)) {
      return _problem(403, 'forbidden', 'missing scope ${op.scope}');
    }
  }
  final schema = operationSchemas[op.id];
  if (schema != null && validateRequests) {
    final bad = _checkRequest(schema, params!, r);
    if (bad != null) return bad;
  }
  final idem = r.method == 'POST' ? r.headers['idempotency-key'] : null;
  final idemKey = idem == null ? null : '${op.id}:$idem';
  if (idemKey != null && _idempotency.containsKey(idemKey)) {
    final (bodyHash, res) = _idempotency[idemKey]!;
    if (bodyHash != _hash(r.body)) {
      return _problem(
        409,
        'conflict',
        'Idempotency-Key reused with another body',
      );
    }
    return res;
  }
  FreedomeResponse res;
  try {
    res = await _route(op, params!, r, principal);
  } on _Http catch (e) {
    res = e.response;
  } on FormatException catch (e) {
    res = _problem(422, 'validation-failed', e.message);
  } on NetworkException {
    rethrow;
  } on FreedomeException catch (e) {
    res = _problem(
      e is ValidationException ? 422 : 400,
      e.code ?? 'validation-failed',
      e.message,
    );
  }
  if (schema != null && strictResponses) _checkResponse(op, schema, res);
  if (idemKey != null && res.ok) _idempotency[idemKey] = (_hash(r.body), res);
  return res;
}