Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions pkgs/google_cloud/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
## 0.4.1-wip

### `bad_request_exception.dart`
### `http_serving.dart`

- Expanded `BadRequestException` to support structured error reporting as
- Introduces `HttpResponseException` and `httpResponseExceptionMiddleware`.
- `BadRequestException` and `badRequestMiddleware` are deprecated in favor of
`HttpResponseException` and `httpResponseExceptionMiddleware`, respectively.
(They are aliased for backward compatibility.)
- Expanded `HttpResponseException` to support structured error reporting as
per AIP-193.
- Added `status` (`String?`) and `details` (`List<Map<String, Object?>>?`)
fields.
- Added `toJson()` method to serialize the error into a standard Google Cloud
error payload.
- Updated `toString()` to include `status` and `details` when non-null.
- Added factory constructors for common HTTP 4XX status codes: `badRequest`
(400), `unauthorized` (401), `forbidden` (403), `notFound` (404),
`conflict` (409), and `tooManyRequests` (429).
- Added factory constructors for common HTTP 4XX and 5XX status codes:
- `badRequest` (400)
- `unauthorized` (401)
- `forbidden` (403)
- `notFound` (404)
- `conflict` (409)
- `tooManyRequests` (429)
- `internalServerError` (500)
- `notImplemented` (501)
- `serviceUnavailable` (503)
- `gatewayTimeout` (504)
- Added default `status` values for factories that map 1:1 to gRPC status
codes (e.g., `unauthorized` defaults to `'UNAUTHENTICATED'`).

### `http_serving.dart` (and internal files)

- Updated `badRequestMiddleware` to leverage `BadRequestException.toJson()` for
JSON responses, returning a standard Google Cloud error payload.
- Updated plain text errors to use `BadRequestException.toString()`.
- Updated `httpResponseExceptionMiddleware` to leverage
`HttpResponseException.toJson()` for JSON responses, returning a standard
Google Cloud error payload.
- Updated plain text errors to use `HttpResponseException.toString()`.

## 0.4.0

Expand Down
2 changes: 1 addition & 1 deletion pkgs/google_cloud/example/example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Handler _onlyGetRootMiddleware(Handler handler) => (Request request) async {
return await handler(request);
}

throw BadRequestException(404, 'Not found');
throw HttpResponseException.notFound();
};

CommitRequest _incrementRequest(String projectId) => CommitRequest(
Expand Down
26 changes: 14 additions & 12 deletions pkgs/google_cloud/lib/general.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@

/// General Google Cloud Platform integration features.
///
/// {@canonicalFor gcp_project.computeProjectId}
/// {@canonicalFor gcp_project.projectIdFromCredentialsFile}
/// {@canonicalFor gcp_project.projectIdFromEnvironmentVariables}
/// {@canonicalFor gcp_project.projectIdFromGcloudConfig}
/// {@canonicalFor gcp_project.MetadataServerException}
/// {@canonicalFor gcp_project.projectIdFromMetadataServer}
/// {@canonicalFor gcp_project.serviceAccountEmailFromMetadataServer}
/// {@canonicalFor logger.LogSeverity}
/// {@canonicalFor logger.CloudLogger}
/// {@canonicalFor structured_logging.structuredLogEntry}
/// {@canonicalFor metadata.gceMetadataHost}
/// {@canonicalFor metadata.gceMetadataUrl}
/// {@canonicalFor general.CloudLogger}
/// {@canonicalFor general.computeProjectId}
/// {@canonicalFor general.fetchMetadataValue}
/// {@canonicalFor general.gceMetadataHost}
/// {@canonicalFor general.gceMetadataUrl}
/// {@canonicalFor general.getMetadataValue}
/// {@canonicalFor general.LogSeverity}
/// {@canonicalFor general.MetadataServerException}
/// {@canonicalFor general.projectIdFromCredentialsFile}
/// {@canonicalFor general.projectIdFromEnvironmentVariables}
/// {@canonicalFor general.projectIdFromGcloudConfig}
/// {@canonicalFor general.projectIdFromMetadataServer}
/// {@canonicalFor general.serviceAccountEmailFromMetadataServer}
/// {@canonicalFor general.structuredLogEntry}
library;

export 'src/gcp_project.dart'
Expand Down
30 changes: 19 additions & 11 deletions pkgs/google_cloud/lib/http_serving.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,35 @@
/// }
/// ```
///
/// {@canonicalFor bad_configuration_exception.BadConfigurationException}
/// {@canonicalFor bad_request_exception.BadRequestException}
/// {@canonicalFor http_logging.badRequestMiddleware}
/// {@canonicalFor http_logging.cloudLoggingMiddleware}
/// {@canonicalFor http_logging.createLoggingMiddleware}
/// {@canonicalFor http_logging.currentLogger}
/// {@canonicalFor serve.listenPortFromEnvironment}
/// {@canonicalFor serve.serveHandler}
/// {@canonicalFor terminate.waitForTerminate}
/// {@canonicalFor http_serving.BadConfigurationException}
/// {@canonicalFor http_serving.badRequestMiddleware}
/// {@canonicalFor http_serving.cloudLoggingMiddleware}
/// {@canonicalFor http_serving.createLoggingMiddleware}
/// {@canonicalFor http_serving.currentLogger}
/// {@canonicalFor http_serving.httpResponseExceptionMiddleware}
/// {@canonicalFor http_serving.BadRequestException}
/// {@canonicalFor http_serving.HttpResponseException}
/// {@canonicalFor http_serving.listenPortFromEnvironment}
/// {@canonicalFor http_serving.serveHandler}
/// {@canonicalFor http_serving.TraceContextData}
/// {@canonicalFor http_serving.waitForTerminate}
library;

export 'src/logger.dart' show CloudLogger, LogSeverity;
export 'src/serving/bad_configuration_exception.dart'
show BadConfigurationException;
export 'src/serving/bad_request_exception.dart' show BadRequestException;
export 'src/serving/http_logging.dart'
show
badRequestMiddleware,
cloudLoggingMiddleware,
createLoggingMiddleware,
currentLogger;
currentLogger,
httpResponseExceptionMiddleware;
export 'src/serving/http_response_exception.dart'
show
// ignore: deprecated_member_use_from_same_package
BadRequestException,
HttpResponseException;
export 'src/serving/serve.dart' show listenPortFromEnvironment, serveHandler;
export 'src/serving/terminate.dart' show waitForTerminate;
export 'src/serving/trace_context_data.dart' show TraceContextData;
65 changes: 34 additions & 31 deletions pkgs/google_cloud/lib/src/serving/http_logging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,53 @@ import 'package:shelf/shelf.dart';
import '../constants.dart';
import '../logger.dart';
import '../structured_logging.dart';
import 'bad_request_exception.dart';
import 'http_response_exception.dart';
import 'trace_context_data.dart';

export '../structured_logging.dart';

const _badRequestExceptionContextKey = 'google_cloud.bad_request_exception';
const _badStackTraceContextKey = 'google_cloud.bad_stack_trace';
const _httpResponseExceptionKey = 'google_cloud.response_exception';
const _exceptionStackTraceKey = 'google_cloud.bad_stack_trace';

/// Convenience [Middleware] that handles logging depending on [projectId].
///
/// [projectId] is the optional Google Cloud Project ID used for trace
/// correlation.
///
/// If [projectId] is `null`, returns [Middleware] composed of [logRequests] and
/// [badRequestMiddleware].
/// [httpResponseExceptionMiddleware].
///
/// If [projectId] is provided, returns the value from [cloudLoggingMiddleware].
Middleware createLoggingMiddleware({String? projectId}) => projectId == null
? _errorWriter.addMiddleware(logRequests()).addMiddleware(_handleBadRequest)
? _errorWriter
.addMiddleware(logRequests())
.addMiddleware(_handleResponseException)
: cloudLoggingMiddleware(projectId);

/// Adds logic which catches [BadRequestException], logs details to [stderr] and
/// returns a corresponding [Response].
Middleware get badRequestMiddleware => _handleBadRequest;
@Deprecated('use httpResponseExceptionMiddleware instead')
Middleware get badRequestMiddleware => httpResponseExceptionMiddleware;

Handler _handleBadRequest(Handler innerHandler) => (request) async {
/// Adds logic which catches [HttpResponseException], logs details to [stderr]
/// and returns a corresponding [Response].
Middleware get httpResponseExceptionMiddleware => _handleResponseException;

Handler _handleResponseException(Handler innerHandler) => (request) async {
try {
final response = await innerHandler(request);
return response;
} on BadRequestException catch (error, stack) {
return _responseFromBadRequest(error, stack, request.headers);
} on HttpResponseException catch (error, stack) {
return _responseFromException(error, stack, request.headers);
}
};

Handler _errorWriter(Handler innerHandler) => (request) async {
final response = await innerHandler(request);

final error =
response.context[_badRequestExceptionContextKey] as BadRequestException?;
response.context[_httpResponseExceptionKey] as HttpResponseException?;

if (error != null) {
final stack = response.context[_badStackTraceContextKey] as StackTrace?;
final stack = response.context[_exceptionStackTraceKey] as StackTrace?;
final output = [
error,
if (error.innerError != null)
Expand All @@ -80,8 +85,8 @@ Handler _errorWriter(Handler innerHandler) => (request) async {
return response;
};

Response _responseFromBadRequest(
BadRequestException e,
Response _responseFromException(
HttpResponseException e,
StackTrace stack, [
Map<String, String>? requestHeaders,
]) {
Expand All @@ -90,20 +95,14 @@ Response _responseFromBadRequest(
e.statusCode,
body: jsonEncode(e.toJson()),
headers: {HttpHeaders.contentTypeHeader: _jsonMimeType},
context: {
_badRequestExceptionContextKey: e,
_badStackTraceContextKey: stack,
},
context: {_httpResponseExceptionKey: e, _exceptionStackTraceKey: stack},
);
}

return Response(
e.statusCode,
body: e.toString(),
context: {
_badRequestExceptionContextKey: e,
_badStackTraceContextKey: stack,
},
context: {_httpResponseExceptionKey: e, _exceptionStackTraceKey: stack},
);
}

Expand Down Expand Up @@ -138,11 +137,12 @@ Middleware cloudLoggingMiddleware(String projectId) {
String createErrorLogEntryFromRequest(
Object error,
StackTrace? stackTrace,
LogSeverity logSeverity,
) => structuredLogEntry(
LogSeverity logSeverity, {
Map<String, Object?>? extraPayload,
}) => structuredLogEntry(
'$error'.trim(),
logSeverity,
payload: traceContext?.asPayloadMap(),
payload: {...?traceContext?.asPayloadMap(), ...?extraPayload},
stackTrace: stackTrace,
);

Expand All @@ -164,11 +164,14 @@ Middleware cloudLoggingMiddleware(String projectId) {
completer.completeError(error, stackTrace);
}

final logContentString = error is BadRequestException
final logContentString = error is HttpResponseException
? createErrorLogEntryFromRequest(
'Bad request. ${error.message}',
error,
error.innerStack ?? stackTrace,
LogSeverity.warning,
error.statusCode >= 500
? LogSeverity.error
: LogSeverity.warning,
extraPayload: error.toJson(),
)
: createErrorLogEntryFromRequest(
error,
Expand All @@ -183,8 +186,8 @@ Middleware cloudLoggingMiddleware(String projectId) {
return;
}

final response = error is BadRequestException
? _responseFromBadRequest(error, stackTrace, request.headers)
final response = error is HttpResponseException
? _responseFromException(error, stackTrace, request.headers)
: Response.internalServerError();

completer.complete(response);
Expand Down
Loading
Loading