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;
}