Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion docs-website/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
"pages": [
"router/configuration",
"router/configuration/config-design",
"router/configuration/template-expressions"
"router/configuration/template-expressions",
"router/configuration/error-types"
]
},
"router/mcp",
Expand Down
30 changes: 30 additions & 0 deletions docs-website/router/configuration/error-types.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: "Error Types"
description: "Router error type classifications for metrics and expressions"
icon: "triangle-exclamation"
---

When an error occurs during request processing, the router classifies it into one of the following types. This classification is available via:
- **Expressions**: `request.errorType` and `subgraph.request.errorType` (see [Template Expressions](/router/configuration/template-expressions))
- **Metrics**: the `wg.error.type` OTEL attribute (or `wg_error_type` in Prometheus)

| Value | Description |
|---|---|
| `rate_limit` | The request was rejected because the client exceeded the configured rate limit. |
| `unauthorized` | The request failed authentication or authorization. |
| `context_canceled` | The request was canceled, typically because the client disconnected before the response was sent. |
| `context_timeout` | The request timed out before a response could be produced. |
| `upgrade_failed` | A WebSocket upgrade request to a subgraph failed. |
| `edfs` | An error occurred in a Cosmo Streams (EDFS) data source. |
| `invalid_ws_subprotocol` | The client requested an unsupported WebSocket subprotocol. |
| `edfs_invalid_message` | A message received from a Cosmo Streams backend could not be parsed as valid JSON. |
| `merge_result` | The router failed to merge results from multiple subgraph responses. |
| `streams_handler_error` | An error occurred while handling a Cosmo Streams event. |
| `operation_blocked` | The operation was rejected by the operation blocker (e.g. blocked operation type or safelist violation). |
| `persisted_operation_not_found` | A persisted operation was requested but could not be found. |
| `validation_error` | The GraphQL operation failed parsing or validation against the schema. |
| `input_error` | The request body was malformed or otherwise invalid (e.g. invalid JSON, missing query). |
| `subgraph_error` | One or more subgraphs returned an error during execution. |
| `unknown` | An error occurred that does not match any known category. |
| `publish_error` | A Cosmo Streams publish operation failed (Kafka, NATS, or Redis). Only appears on stream produce/consume metrics. |
| `request_error` | A Cosmo Streams NATS request-reply operation failed. Only appears on stream produce/consume metrics. |
18 changes: 18 additions & 0 deletions docs-website/router/configuration/template-expressions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ The `request` object is a read-only entity that provides details about the incom
- `request.url.port`
- `request.trace.sampled`
- `request.error`
- `request.errorType` — The [error type classification](#error-types) as a string. Only set when an error occurred.

### Operation Object

Expand Down Expand Up @@ -163,6 +164,7 @@ It contains the following properties:
#### SubgraphRequest Properties

- `subgraph.request.error` (error): Any error that occurred during the subgraph request
- `subgraph.request.errorType` (string): The [error type classification](#error-types) for the subgraph request. Only set when an error occurred.
- `subgraph.request.clientTrace` (ClientTrace): Contains tracing information about the client connection

#### SubgraphResponse Properties
Expand Down Expand Up @@ -242,6 +244,22 @@ request.error != nil ? response.body.raw : ''
You can use expressions to specify the conditions for retries upon subgraph request failures. However, this uses a different expression context, which can be found [here](/router/traffic-shaping/retry#conditional-retry-with-expressions).


### Error Types

See the [Error Types](/router/configuration/error-types) reference for the full list of error classifications available via `request.errorType` and `subgraph.request.errorType`.

#### Example expressions

```bash
# Log the response body only for unauthorized errors
request.errorType == 'unauthorized' ? response.body.raw : ''
```

```bash
# Check if a subgraph had a timeout
subgraph.request.errorType == 'context_timeout'
```

### Additional Notes

- A Request for Comments (RFC) is [open](https://github.com/wundergraph/cosmo/pull/1481) for feedback on the complete API specification. Future implementations will be driven by customer requirements.
Expand Down
4 changes: 3 additions & 1 deletion docs-website/router/metrics-and-monitoring.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ You can use the `router.http.requests.error` metric to track errors across route

* `http.status_code`: The status code of the response.

* `wg.error.type`: Classifies the error into a specific category. This attribute is attached to both router-level and subgraph-level request metrics when an error occurs. Only set when an error is present. See [Error Types](/router/configuration/error-types) for the full list of possible values.

You have two ways to query for errors. Both metrics can be quite useful depending on the query scenario. In general, we recommend to use the `router.http.requests.error` metric.

#### Subgraph specific
Expand Down Expand Up @@ -283,7 +285,7 @@ The name of the destination of the messaging backend (topic, queue, etc)
The provider id as specified in the router configuration

* `wg.error.type`:
A generic error type to indicate an error occurred. This is available only for `router.streams.sent.messages` at this moment, as we do not catch message receive errors.
The error type classification. For Cosmo Streams metrics, this indicates a send error. See [Error Types](/router/configuration/error-types) for the full list of possible values.


### Connection Metrics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ The name of the destination of the messaging backend (topic, queue, etc)
The provider id as specified in the router configuration

* `wg_error_type`:
A generic error type to indicate an error occurred. This is available only for `router.streams.sent.messages` at this moment, as we do not catch message receive errors.
The error type classification. For Cosmo Streams metrics, this indicates a send error. See [Error Types](/router/configuration/error-types) for the full list of possible values.

#### Examples

Expand Down
6 changes: 6 additions & 0 deletions router-tests/telemetry/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8463,6 +8463,7 @@ func TestFlakyTelemetry(t *testing.T) {
attribute.StringSlice("services", []string{"employees", "products"}),
attribute.StringSlice("error_services", []string{"products"}),
otel.WgRequestError.Bool(true),
otel.WgErrorType.String("unknown"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Assert subgraph_error instead of unknown for this deterministic subgraph-failure path.

At Line 8466, Line 8541, Line 8862, Line 9046, Line 9121, and Line 9442, the test expects wg.error.type="unknown" even though this scenario is an explicit subgraph fetch failure. That weakens coverage for the new error taxonomy and can hide regressions in subgraph classification.

Suggested expectation fix
- otel.WgErrorType.String("unknown"),
+ otel.WgErrorType.String("subgraph_error"),

Also applies to: 8541-8541, 8862-8862, 9046-9046, 9121-9121, 9442-9442

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router-tests/telemetry/telemetry_test.go` at line 8466, Replace the weak
"unknown" expectation for the deterministic subgraph-failure path with the
explicit subgraph error token: update the occurrences of
otel.WgErrorType.String("unknown") in telemetry_test.go (the assertions at the
locations referenced) to assert otel.WgErrorType.String("subgraph_error")
instead so the test verifies the subgraph fetch failure classification.

otel.WgClientName.String("unknown"),
otel.WgClientVersion.String("missing"),
otel.WgFederatedGraphID.String("graph"),
Expand Down Expand Up @@ -8537,6 +8538,7 @@ func TestFlakyTelemetry(t *testing.T) {
semconv.HTTPStatusCode(200),
attribute.StringSlice("services", []string{"employees", "products"}),
otel.WgRequestError.Bool(true),
otel.WgErrorType.String("unknown"),
attribute.String("sha256", "b0066f89f91315b4610ed127be677e6cea380494eb20c83cc121c97552ca44b2"),
otel.WgClientName.String("unknown"),
otel.WgClientVersion.String("missing"),
Expand Down Expand Up @@ -8857,6 +8859,7 @@ func TestFlakyTelemetry(t *testing.T) {
otel.WgRouterClusterName.String(""),
otel.WgRouterConfigVersion.String(xEnv.RouterConfigVersionMain()),
otel.WgRouterVersion.String("dev"),
otel.WgErrorType.String("unknown"),
),
Value: 1,
},
Expand Down Expand Up @@ -9040,6 +9043,7 @@ func TestFlakyTelemetry(t *testing.T) {
attribute.StringSlice("services", []string{"employees", "products"}),
attribute.StringSlice("error_services", []string{"products"}),
otel.WgRequestError.Bool(true),
otel.WgErrorType.String("unknown"),
otel.WgClientName.String("unknown"),
otel.WgClientVersion.String("missing"),
otel.WgFederatedGraphID.String("graph"),
Expand Down Expand Up @@ -9114,6 +9118,7 @@ func TestFlakyTelemetry(t *testing.T) {
semconv.HTTPStatusCode(200),
attribute.StringSlice("services", []string{"employees", "products"}),
otel.WgRequestError.Bool(true),
otel.WgErrorType.String("unknown"),
attribute.String("sha256", "b0066f89f91315b4610ed127be677e6cea380494eb20c83cc121c97552ca44b2"),
otel.WgClientName.String("unknown"),
otel.WgClientVersion.String("missing"),
Expand Down Expand Up @@ -9434,6 +9439,7 @@ func TestFlakyTelemetry(t *testing.T) {
otel.WgRouterClusterName.String(""),
otel.WgRouterConfigVersion.String(xEnv.RouterConfigVersionMain()),
otel.WgRouterVersion.String("dev"),
otel.WgErrorType.String("unknown"),
),
Value: 1,
},
Expand Down
1 change: 1 addition & 0 deletions router/core/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ func (c *requestContext) SetCustomFieldValueRenderer(renderer resolve.FieldValue
func (c *requestContext) SetError(err error) {
c.error = err
c.expressionContext.Request.Error = err
c.expressionContext.Request.ErrorType = getErrorType(err).String()
}

func (c *requestContext) Operation() OperationContext {
Expand Down
7 changes: 6 additions & 1 deletion router/core/engine_loader_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ func (f *engineLoaderHooks) OnFinished(ctx context.Context, ds resolve.DataSourc
exprCtx.Subgraph.Id = ds.ID
exprCtx.Subgraph.Name = ds.Name
exprCtx.Subgraph.Request.Error = WrapExprError(responseInfo.Err)
exprCtx.Subgraph.Request.ErrorType = getErrorType(responseInfo.Err).String()
Comment on lines 176 to +177

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't populate subgraph.request.errorType on successful fetches.

When responseInfo.Err is nil, this writes "unknown" into the expression context. Because exprCtx is fed into addExpressions(...) immediately below, successful subgraph calls can now look like uncategorized failures in templates, dashboards, and custom telemetry. Leave the field empty unless there is an actual error.

Proposed fix
 	exprCtx.Subgraph.Id = ds.ID
 	exprCtx.Subgraph.Name = ds.Name
 	exprCtx.Subgraph.Request.Error = WrapExprError(responseInfo.Err)
-	exprCtx.Subgraph.Request.ErrorType = getErrorType(responseInfo.Err).String()
+	if responseInfo.Err != nil {
+		exprCtx.Subgraph.Request.ErrorType = getErrorType(responseInfo.Err).String()
+	} else {
+		exprCtx.Subgraph.Request.ErrorType = ""
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exprCtx.Subgraph.Request.Error = WrapExprError(responseInfo.Err)
exprCtx.Subgraph.Request.ErrorType = getErrorType(responseInfo.Err).String()
exprCtx.Subgraph.Id = ds.ID
exprCtx.Subgraph.Name = ds.Name
exprCtx.Subgraph.Request.Error = WrapExprError(responseInfo.Err)
if responseInfo.Err != nil {
exprCtx.Subgraph.Request.ErrorType = getErrorType(responseInfo.Err).String()
} else {
exprCtx.Subgraph.Request.ErrorType = ""
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router/core/engine_loader_hooks.go` around lines 176 - 177, The code
currently sets exprCtx.Subgraph.Request.ErrorType unconditionally using
getErrorType(responseInfo.Err).String(), which writes "unknown" when
responseInfo.Err is nil; change the logic in the block handling responseInfo to
only set exprCtx.Subgraph.Request.ErrorType when responseInfo.Err != nil (and
leave it as the empty string otherwise). Specifically, wrap the
getErrorType(...) assignment in a conditional check on responseInfo.Err (use
responseInfo.Err != nil) alongside the existing exprCtx.Subgraph.Request.Error =
WrapExprError(responseInfo.Err) handling, and ensure addExpressions(...)
receives an expression context that has an empty ErrorType for successful
fetches.


if value := ctx.Value(rcontext.FetchTimingKey); value != nil {
if fetchTiming, ok := value.(*atomic.Int64); ok {
Expand Down Expand Up @@ -293,7 +294,11 @@ func (f *engineLoaderHooks) OnFinished(ctx context.Context, ds resolve.DataSourc
metricSliceAttrs = append(metricSliceAttrs, attribute.StringSlice(v, errorCodesAttr))
}

f.metricStore.MeasureRequestError(ctx, metricSliceAttrs, metricAddOpt)
errType := getErrorType(responseInfo.Err)
errorTypeAttr := rotel.WgErrorType.String(errType.String())

metricAttrs = append(metricAttrs, errorTypeAttr)
f.metricStore.MeasureRequestError(ctx, metricSliceAttrs, otelmetric.WithAttributeSet(attribute.NewSet(metricAttrs...)))

metricAttrs = append(metricAttrs, rotel.WgRequestError.Bool(true))

Expand Down
60 changes: 60 additions & 0 deletions router/core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,50 @@ const (
errorTypeEDFSInvalidMessage
errorTypeMergeResult
errorTypeStreamsHandlerError
errorTypeOperationBlocked
errorTypePersistedOperationNotFound
errorTypeValidationError
errorTypeInputError
errorTypeSubgraphError
)

func (e errorType) String() string {
switch e {
case errorTypeRateLimit:
return "rate_limit"
case errorTypeUnauthorized:
return "unauthorized"
case errorTypeContextCanceled:
return "context_canceled"
case errorTypeContextTimeout:
return "context_timeout"
case errorTypeUpgradeFailed:
return "upgrade_failed"
case errorTypeEDFS:
return "edfs"
case errorTypeInvalidWsSubprotocol:
return "invalid_ws_subprotocol"
case errorTypeEDFSInvalidMessage:
return "edfs_invalid_message"
case errorTypeMergeResult:
return "merge_result"
case errorTypeStreamsHandlerError:
return "streams_handler_error"
case errorTypeOperationBlocked:
return "operation_blocked"
case errorTypePersistedOperationNotFound:
return "persisted_operation_not_found"
case errorTypeValidationError:
return "validation_error"
case errorTypeInputError:
return "input_error"
case errorTypeSubgraphError:
return "subgraph_error"
default:
return "unknown"
}
}

type (
GraphQLErrorResponse struct {
Errors []graphqlError `json:"errors"`
Expand All @@ -64,6 +106,10 @@ func getErrorType(err error) errorType {
if errors.Is(err, context.Canceled) {
return errorTypeContextCanceled
}
var poNotFoundErr *persistedoperation.PersistentOperationNotFoundError
if errors.As(err, &poNotFoundErr) {
return errorTypePersistedOperationNotFound
}
var upgradeErr *graphql_datasource.UpgradeRequestError
if errors.As(err, &upgradeErr) {
return errorTypeUpgradeFailed
Expand Down Expand Up @@ -94,6 +140,20 @@ func getErrorType(err error) errorType {
if errors.As(err, &mergeResultErr) {
return errorTypeMergeResult
}
var reportErr ReportError
if errors.As(err, &reportErr) {
return errorTypeValidationError
}
var subgraphErr *resolve.SubgraphError
if errors.As(err, &subgraphErr) {
return errorTypeSubgraphError
}
var httpErr *httpGraphqlError
if errors.As(err, &httpErr) {
if httpErr.errorCategory != errorTypeUnknown {
return httpErr.errorCategory
}
}
return errorTypeUnknown
}

Expand Down
Loading
Loading