Atlas › Test

TestHandlerErrorRetryBehavior

Exact test identity: go.temporal.io/server/common/nexus/nexusrpc/TestHandlerErrorRetryBehavior

Package
go.temporal.io/server/common/nexus/nexusrpc
Suite / test hierarchy
TestHandlerErrorRetryBehavior
Test
TestHandlerErrorRetryBehavior
Introduced at
client.go ×1 Frontier kind: Joint frontier
Covered ranges
121
Covered lines
330
Covered files
4

Covered source

Expand a file to inspect source; the > gutter marks covered lines.

go.temporal.io/server/common/nexus/nexusrpc/client.go 117 covered LOC · 44 ranges

Open complete file

87 }
88
89 > func (c *baseHTTPClient) failureFromResponse(response *http.Response, body []byte) (nexus.Failure, error) { client.go
90 > if !isMediaTypeJSON(response.Header.Get("Content-Type")) {
91 return nexus.Failure{}, newUnexpectedResponseError(fmt.Sprintf("invalid response content type: %q", response.Header.Get("Content-Type")), response, body)
92 }
93 > var failure nexus.Failure client.go
94 > err := json.Unmarshal(body, &failure)
95 > return failure, err
96 }
97
98 > func (c *baseHTTPClient) defaultErrorFromResponse(response *http.Response, body []byte, cause error) error { client.go
99 > errorType, err := httpStatusCodeToHandlerErrorType(response)
100 > if err != nil {
101 // TODO(bergundy): optimization - use the provided cause, it's already a deserialized failure.
102 return newUnexpectedResponseError(err.Error(), response, body)
103 }
104 > handlerErr := &nexus.HandlerError{ client.go
105 > Type: errorType,
106 > // For compatibility with older servers.
107 > RetryBehavior: retryBehaviorFromHeader(response.Header),
108 > Cause: cause,
109 > }
110 >
111 > // Ensure the original failure is available, the calling code expects it.
112 > originalFailure, err := c.failureConverter.ErrorToFailure(handlerErr)
113 > if err != nil {
114 return newUnexpectedResponseError("failed to construct handler error from response: "+err.Error(), response, body)
115 }
116 > handlerErr.OriginalFailure = &originalFailure client.go
117 > return handlerErr
118 }
119
120 // bestEffortHandlerErrorFromResponse attempts to read a handler error from the response, but falls back to an unexpected response error.
121 > func (c *baseHTTPClient) bestEffortHandlerErrorFromResponse(response *http.Response, body []byte) error { client.go
122 > failure, err := c.failureFromResponse(response, body)
123 > if err != nil {
124 return c.defaultErrorFromResponse(response, body, nil)
125 }
126 > convErr, err := c.failureConverter.FailureToError(failure) client.go
127 > if err != nil {
128 return newUnexpectedResponseError(fmt.Sprintf("failed to convert Failure to error: %s", err.Error()), response, body)
129 }
130 > if _, ok := convErr.(*nexus.HandlerError); !ok { client.go
131 > convErr = c.defaultErrorFromResponse(response, body, convErr) client.go
132 > }
133 > return convErr client.go
134 }
135
152 // NewHTTPClient creates a new [HTTPClient] from provided [HTTPClientOptions].
153 // BaseURL and Service are required.
154 > func NewHTTPClient(options HTTPClientOptions) (*HTTPClient, error) { client.go
155 > if options.HTTPCaller == nil {
156 > options.HTTPCaller = http.DefaultClient.Do
157 > }
158 > if options.BaseURL == "" {
159 return nil, errors.New("empty BaseURL")
160 }
161 > if options.Service == "" { client.go
162 return nil, errors.New("empty Service")
163 }
164 > var baseURL *url.URL client.go
165 > var err error
166 > baseURL, err = url.Parse(options.BaseURL)
167 > if err != nil {
168 return nil, err
169 }
170 > if baseURL.Scheme != "http" && baseURL.Scheme != "https" { client.go
171 return nil, fmt.Errorf("invalid URL scheme: %s", baseURL.Scheme)
172 }
173 > if options.Serializer == nil { client.go
174 > options.Serializer = nexus.DefaultSerializer() client.go
175 > }
176 > if options.FailureConverter == nil { client.go
177 > options.FailureConverter = DefaultFailureConverter() client.go
178 > }
179 > return &HTTPClient{ client.go
180 > baseHTTPClient: baseHTTPClient{
181 > serializer: options.Serializer,
182 > failureConverter: options.FailureConverter,
183 > httpCaller: options.HTTPCaller,
184 > },
185 > serviceBaseURL: baseURL,
186 > service: options.Service,
187 > }, nil
188 }
189
225 input any,
226 options nexus.StartOperationOptions,
227 > ) (*ClientStartOperationResponse[*nexus.LazyValue], error) { client.go
228 > var reader *nexus.Reader
229 > var contentLength *int64
230 > if r, ok := input.(*nexus.Reader); ok {
231 // Close the input reader in case we error before sending the HTTP request (which may double close but
232 // that's fine since we ignore the error).
234 defer r.Close()
235 reader = r
236 > } else { client.go
237 > content, ok := input.(*nexus.Content) client.go
238 > if !ok {
239 > var err error client.go
240 > content, err = c.serializer.Serialize(input)
241 > if err != nil {
242 return nil, err
243 }
244 }
245 > header := maps.Clone(content.Header) client.go
246 > if header == nil {
247 header = make(nexus.Header, 1)
248 }
249 > contentLength = new(int64) client.go
250 > *contentLength = int64(len(content.Data))
251 >
252 > reader = &nexus.Reader{
253 > ReadCloser: io.NopCloser(bytes.NewReader(content.Data)),
254 > Header: header,
255 > }
256 }
257
258 > url := c.serviceBaseURL.JoinPath(url.PathEscape(c.service), url.PathEscape(operation)) client.go
259 >
260 > if options.CallbackURL != "" {
261 q := url.Query()
262 q.Set(queryCallbackURL, options.CallbackURL)
263 url.RawQuery = q.Encode()
264 }
265 > request, err := http.NewRequestWithContext(ctx, "POST", url.String(), reader) client.go
266 > if contentLength != nil {
267 > request.ContentLength = *contentLength client.go
268 > }
269 > if err != nil { client.go
270 return nil, err
271 }
272
273 > if options.RequestID == "" { client.go
274 > options.RequestID = uuid.NewString() client.go
275 > }
276 > request.Header.Set(headerRequestID, options.RequestID) client.go
277 > request.Header.Set(headerUserAgent, userAgent)
278 > addContentHeaderToHTTPHeader(reader.Header, request.Header)
279 > addCallbackHeaderToHTTPHeader(options.CallbackHeader, request.Header)
280 > if err := addLinksToHTTPHeader(options.Links, request.Header); err != nil {
281 return nil, fmt.Errorf("failed to serialize links into header: %w", err)
282 }
283 > addContextTimeoutToHTTPHeader(ctx, request.Header) client.go
284 > addNexusHeaderToHTTPHeader(options.Header, request.Header)
285 >
286 > response, err := c.httpCaller(request)
287 > if err != nil {
288 return nil, err
289 }
290
291 > links, err := getLinksFromHeader(response.Header) client.go
292 > if err != nil {
293 // Have to read body here to check if it is a Failure.
294 body, err := readAndReplaceBody(response)
308
309 // Do not close response body here to allow successful result to read it.
310 > if response.StatusCode == http.StatusOK { client.go
311 return &ClientStartOperationResponse[*nexus.LazyValue]{
312 Successful: nexus.NewLazyValue(
322
323 // Do this once here and make sure it doesn't leak.
324 > body, err := readAndReplaceBody(response) client.go
325 > if err != nil {
326 return nil, err
327 }
328
329 > switch response.StatusCode { client.go
330 case http.StatusCreated:
331 info, err := operationInfoFromResponse(response, body)
373
374 return nil, wireErr
375 > default: client.go
376 > return nil, c.bestEffortHandlerErrorFromResponse(response, body)
377 }
378 }
402 // body with an in-memory buffer.
403 // The body is replaced even when there was an error reading the entire body.
404 > func readAndReplaceBody(response *http.Response) ([]byte, error) { client.go
405 > responseBody := response.Body
406 > body, err := io.ReadAll(responseBody)
407 > if err := responseBody.Close(); err != nil {
408 return nil, err
409 }
410 > response.Body = io.NopCloser(bytes.NewReader(body)) client.go
411 > return body, err
412 }
413
423 }
424
425 > func httpStatusCodeToHandlerErrorType(response *http.Response) (nexus.HandlerErrorType, error) { client.go
426 > switch response.StatusCode {
427 case http.StatusBadRequest:
428 return nexus.HandlerErrorTypeBadRequest, nil
439 case http.StatusTooManyRequests:
440 return nexus.HandlerErrorTypeResourceExhausted, nil
441 > case http.StatusInternalServerError: client.go
442 > return nexus.HandlerErrorTypeInternal, nil
443 case http.StatusNotImplemented:
444 return nexus.HandlerErrorTypeNotImplemented, nil
452 }
453
454 > func retryBehaviorFromHeader(header http.Header) nexus.HandlerErrorRetryBehavior { client.go
455 > switch strings.ToLower(header.Get(headerRetryable)) {
456 case "true":
457 return nexus.HandlerErrorRetryBehaviorRetryable
458 > case "false": client.go
459 > return nexus.HandlerErrorRetryBehaviorNonRetryable
460 default:
461 return nexus.HandlerErrorRetryBehaviorUnspecified
go.temporal.io/server/common/nexus/nexusrpc/server.go 109 covered LOC · 36 ranges

Open complete file

93 // status code based on the type of error.
94 // nolint:revive // Keeping all of the logic together for readability, even if it means the function is long.
95 > func (h *BaseHTTPHandler) WriteFailure(writer http.ResponseWriter, r *http.Request, err error) { server.go
96 > var failure nexus.Failure
97 > var failureError *nexus.FailureError
98 > var opError *nexus.OperationError
99 > var handlerError *nexus.HandlerError
100 > var operationState nexus.OperationState
101 > statusCode := http.StatusInternalServerError
102 >
103 > if errors.As(err, &opError) {
104 operationState = opError.State
105 var convErr error
123 }
124 writer.Header().Set(headerOperationState, string(operationState))
125 > } else if errors.As(err, &handlerError) { server.go
126 > var convErr error
127 > failure, convErr = h.FailureConverter.ErrorToFailure(handlerError)
128 > if convErr != nil {
129 h.Logger.Error("failed to convert handler error to failure", "error", convErr)
130 writer.WriteHeader(http.StatusInternalServerError)
132 }
133 // Backward compatibility, unwrap the failure cause.
134 > if r.Header.Get(HeaderTemporalNexusFailureSupport) != "true" && failure.Cause != nil { server.go
135 > failure = *failure.Cause server.go
136 > }
137 > switch handlerError.Type { server.go
138 case nexus.HandlerErrorTypeBadRequest:
139 statusCode = http.StatusBadRequest
150 case nexus.HandlerErrorTypeResourceExhausted:
151 statusCode = http.StatusTooManyRequests
152 > case nexus.HandlerErrorTypeInternal: server.go
153 > statusCode = http.StatusInternalServerError
154 case nexus.HandlerErrorTypeNotImplemented:
155 statusCode = http.StatusNotImplemented
170 }
171
172 > b, err := json.Marshal(failure) server.go
173 > if err != nil {
174 h.Logger.Error("failed to marshal failure", "error", err)
175 writer.WriteHeader(http.StatusInternalServerError)
176 return
177 }
178 > writer.Header().Set("Content-Type", contentTypeJSON) server.go
179 >
180 > // Set the retry header here after ensuring that we don't fail with internal error due to failed marshaling to
181 > // preserve the user's intent.
182 > if handlerError != nil {
183 > switch handlerError.RetryBehavior { server.go
184 > case nexus.HandlerErrorRetryBehaviorNonRetryable: server.go
185 > writer.Header().Set(headerRetryable, "false")
186 case nexus.HandlerErrorRetryBehaviorRetryable:
187 writer.Header().Set(headerRetryable, "true")
191 }
192
193 > writer.WriteHeader(statusCode) server.go
194 >
195 > if _, err := writer.Write(b); err != nil {
196 h.Logger.Error("failed to write response body", "error", err)
197 }
198 }
199
200 > func (h *httpHandler) startOperation(service, operation string, writer http.ResponseWriter, request *http.Request) { server.go
201 > links, err := getLinksFromHeader(request.Header)
202 > if err != nil {
203 h.WriteFailure(writer, request, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid %q header", headerLink))
204 return
205 }
206 > options := nexus.StartOperationOptions{ server.go
207 > RequestID: request.Header.Get(headerRequestID),
208 > CallbackURL: request.URL.Query().Get(queryCallbackURL),
209 > CallbackHeader: prefixStrippedHTTPHeaderToNexusHeader(request.Header, "nexus-callback-"),
210 > Header: httpHeaderToNexusHeader(request.Header, "content-", "nexus-callback-"),
211 > Links: links,
212 > }
213 > value := nexus.NewLazyValue(
214 > h.options.Serializer,
215 > &nexus.Reader{
216 > ReadCloser: request.Body,
217 > Header: prefixStrippedHTTPHeaderToNexusHeader(request.Header, "content-"),
218 > },
219 > )
220 >
221 > ctx, cancel, ok := h.contextWithTimeoutFromHTTPRequest(writer, request)
222 > if !ok {
223 return
224 }
225 > defer cancel() server.go
226 >
227 > ctx = nexus.WithHandlerContext(ctx, nexus.HandlerInfo{
228 > Service: service,
229 > Operation: operation,
230 > Header: options.Header,
231 > })
232 > response, err := h.options.Handler.StartOperation(ctx, service, operation, value, options)
233 > if err != nil {
234 > h.WriteFailure(writer, request, err) server.go
235 > } else { server.go
236 if err := addLinksToHTTPHeader(nexus.HandlerLinks(ctx), writer.Header()); err != nil {
237 h.Logger.Error("failed to serialize links into header", "error", err)
270 // Returns (0, true) if unset. Returns ({parsedDuration}, true) if set. If set and there is an error parsing the
271 // duration, it writes a failure response and returns (0, false).
272 > func (h *httpHandler) parseRequestTimeoutHeader(writer http.ResponseWriter, request *http.Request) (time.Duration, bool) { server.go
273 > timeoutStr := request.Header.Get(nexus.HeaderRequestTimeout)
274 > if timeoutStr != "" {
275 > timeoutDuration, err := ParseDuration(timeoutStr) server.go
276 > if err != nil {
277 h.Logger.Warn("invalid request timeout header", "timeout", timeoutStr)
278 h.WriteFailure(writer, request, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid request timeout header"))
279 return 0, false
280 }
281 > return timeoutDuration, true server.go
282 }
283 return 0, true
286 // contextWithTimeoutFromHTTPRequest extracts the context from the HTTP request and applies the timeout indicated by
287 // the Request-Timeout header, if set.
288 > func (h *httpHandler) contextWithTimeoutFromHTTPRequest(writer http.ResponseWriter, request *http.Request) (context.Context, context.CancelFunc, bool) { server.go
289 > requestTimeout, ok := h.parseRequestTimeoutHeader(writer, request)
290 > if !ok {
291 return nil, nil, false
292 }
293 > if requestTimeout > 0 { server.go
294 > ctx, cancel := context.WithTimeout(request.Context(), requestTimeout) server.go
295 > return ctx, cancel, true
296 > }
297 return request.Context(), func() {}, true
298 }
318 }
319
320 > func (h *httpHandler) handleRequest(writer http.ResponseWriter, request *http.Request) { server.go
321 > if request.Method != "POST" {
322 h.WriteFailure(writer, request, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid request method: expected POST, got %q", request.Method))
323 return
324 }
325 > parts := strings.Split(request.URL.EscapedPath(), "/") server.go
326 > // First part is empty (due to leading /)
327 > if len(parts) < 3 {
328 h.WriteFailure(writer, request, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "not found"))
329 return
330 }
331 > service, err := url.PathUnescape(parts[1]) server.go
332 > if err != nil {
333 h.WriteFailure(writer, request, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "failed to parse URL path"))
334 return
335 }
336 > operation, err := url.PathUnescape(parts[2]) server.go
337 > if err != nil {
338 h.WriteFailure(writer, request, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "failed to parse URL path"))
339 return
341
342 // First handle StartOperation at /{service}/{operation}
343 > if len(parts) == 3 { server.go
344 > h.startOperation(service, operation, writer, request) server.go
345 > return
346 > }
347
348 // Handle deprecated /{service}/{operation}/{operation_token}/cancel
380
381 // NewHTTPHandler constructs an [http.Handler] from given options for handling Nexus service requests.
382 > func NewHTTPHandler(options HandlerOptions) http.Handler { server.go
383 > if options.Logger == nil {
384 > options.Logger = slog.Default() server.go
385 > }
386 > if options.GetResultTimeout == 0 { server.go
387 options.GetResultTimeout = time.Minute
388 }
389 > if options.Serializer == nil { server.go
390 > options.Serializer = nexus.DefaultSerializer() server.go
391 > }
392 > if options.FailureConverter == nil { server.go
393 > options.FailureConverter = DefaultFailureConverter() server.go
394 > }
395 > handler := &httpHandler{ server.go
396 > BaseHTTPHandler: BaseHTTPHandler{
397 > Logger: options.Logger,
398 > FailureConverter: options.FailureConverter,
399 > },
400 > options: options,
401 > }
402 >
403 > return http.HandlerFunc(handler.handleRequest)
404 }
go.temporal.io/server/common/nexus/nexusrpc/api.go 53 covered LOC · 24 ranges

Open complete file

43 const statusOperationUnsuccessful = http.StatusFailedDependency
44
45 > func isMediaTypeJSON(contentType string) bool { api.go
46 > if contentType == "" {
47 return false
48 }
49 > mediaType, _, err := mime.ParseMediaType(contentType) api.go
50 > return err == nil && mediaType == "application/json"
51 }
52
53 > func prefixStrippedHTTPHeaderToNexusHeader(httpHeader http.Header, prefix string) nexus.Header { api.go
54 > header := nexus.Header{}
55 > for k, v := range httpHeader {
56 > lowerK := strings.ToLower(k)
57 > if strings.HasPrefix(lowerK, prefix) {
58 // Nexus headers can only have single values, ignore multiple values.
59 header[lowerK[len(prefix):]] = v[0]
60 }
61 }
62 > return header api.go
63 }
64
65 > func addContentHeaderToHTTPHeader(nexusHeader nexus.Header, httpHeader http.Header) http.Header { api.go
66 > for k, v := range nexusHeader {
67 httpHeader.Set("Content-"+k, v)
68 }
69 > return httpHeader api.go
70 }
71
72 > func addCallbackHeaderToHTTPHeader(nexusHeader nexus.Header, httpHeader http.Header) http.Header { api.go
73 > for k, v := range nexusHeader {
74 httpHeader.Set("Nexus-Callback-"+k, v)
75 }
76 > return httpHeader api.go
77 }
78
79 > func addLinksToHTTPHeader(links []nexus.Link, httpHeader http.Header) error { api.go
80 > for _, link := range links {
81 encodedLink, err := encodeLink(link)
82 if err != nil {
85 httpHeader.Add(headerLink, encodedLink)
86 }
87 > return nil api.go
88 }
89
90 > func getLinksFromHeader(httpHeader http.Header) ([]nexus.Link, error) { api.go
91 > var links []nexus.Link
92 > headerValues := httpHeader.Values(headerLink)
93 > if len(headerValues) == 0 {
94 > return nil, nil api.go
95 > }
96 for encodedLink := range strings.SplitSeq(strings.Join(headerValues, ","), ",") {
97 link, err := decodeLink(encodedLink)
104 }
105
106 > func httpHeaderToNexusHeader(httpHeader http.Header, excludePrefixes ...string) nexus.Header { api.go
107 > header := nexus.Header{}
108 > headerLoop:
109 > for k, v := range httpHeader {
110 > lowerK := strings.ToLower(k)
111 > for _, prefix := range excludePrefixes {
112 > if strings.HasPrefix(lowerK, prefix) { api.go
113 continue headerLoop
114 }
115 }
116 // Nexus headers can only have single values, ignore multiple values.
117 > header[lowerK] = v[0] api.go
118 }
119 > return header api.go
120 }
121
122 > func addNexusHeaderToHTTPHeader(nexusHeader nexus.Header, httpHeader http.Header) http.Header { api.go
123 > for k, v := range nexusHeader {
124 httpHeader.Set(k, v)
125 }
126 > return httpHeader api.go
127 }
128
129 > func addContextTimeoutToHTTPHeader(ctx context.Context, httpHeader http.Header) http.Header { api.go
130 > deadline, ok := ctx.Deadline()
131 > if !ok {
132 return httpHeader
133 }
134 > httpHeader.Set(nexus.HeaderRequestTimeout, FormatDuration(time.Until(deadline))) api.go
135 > return httpHeader
136 }
137
254 var durationRegexp = regexp.MustCompile(`^(\d+(?:\.\d+)?)(ms|s|m)$`)
255
256 > func ParseDuration(value string) (time.Duration, error) { api.go
257 > m := durationRegexp.FindStringSubmatch(value)
258 > if len(m) == 0 {
259 return 0, fmt.Errorf("invalid duration: %q", value)
260 }
261 > v, err := strconv.ParseFloat(m[1], 64) api.go
262 > if err != nil {
263 return 0, err
264 }
265
266 > switch m[2] { api.go
267 > case "ms":
268 > return time.Millisecond * time.Duration(v), nil
269 case "s":
270 return time.Millisecond * time.Duration(v*1e3), nil
277
278 // FormatDuration converts a duration into a string representation in millisecond resolution.
279 > func FormatDuration(d time.Duration) string { api.go
280 > return strconv.FormatInt(d.Milliseconds(), 10) + "ms"
281 > }
282
283 // MarkAsWrapperError adds the "unwrap-error" metadata to the original failure of the given OperationError, which
go.temporal.io/server/common/nexus/nexusrpc/failure_converter.go 51 covered LOC · 17 ranges

Open complete file

46 // ErrorToFailure implements FailureConverter.
47 // nolint:revive // Keeping all of the logic together for readability, even if it means the function is long.
48 > func (e knownErrorFailureConverter) ErrorToFailure(err error) (nexus.Failure, error) { failure_converter.go
49 > if err == nil {
50 return nexus.Failure{}, nil
51 }
52 // NOTE: not using errors.Unwrap here we are intentionally only supporting unwrapping known errors.
53 > switch typedErr := err.(type) { failure_converter.go
54 > case *nexus.FailureError: failure_converter.go
55 > f := typedErr.Failure
56 > // FailureError is has both a Cause error and an underlying Failure Cause.
57 > // When instantiated directly, there are cases where only the Go error cause is set.
58 > // Preserve the embedded failure's cause, as the embedded failure is treated similarly to the OriginalFailure field for well-known other error types.
59 > if typedErr.Cause != nil && f.Cause == nil {
60 c, err := e.ErrorToFailure(typedErr.Cause)
61 if err != nil {
64 f.Cause = &c
65 }
66 > return f, nil failure_converter.go
67 > case *nexus.HandlerError: failure_converter.go
68 > if typedErr.OriginalFailure != nil {
69 return *typedErr.OriginalFailure, nil
70 }
71 > data := serializedHandlerError{ failure_converter.go
72 > Type: string(typedErr.Type),
73 > RetryableOverride: retryBehaviorAsOptionalBool(typedErr),
74 > }
75 > var details []byte
76 > details, err := json.Marshal(data)
77 > if err != nil {
78 return nexus.Failure{}, err
79 }
80 > f := nexus.Failure{ failure_converter.go
81 > Message: typedErr.Message,
82 > StackTrace: typedErr.StackTrace,
83 > Metadata: map[string]string{
84 > "type": "nexus.HandlerError",
85 > },
86 > Details: details,
87 > }
88 >
89 > if typedErr.Cause != nil {
90 > c, err := e.ErrorToFailure(typedErr.Cause) failure_converter.go
91 > if err != nil {
92 return nexus.Failure{}, err
93 }
94 > f.Cause = &c failure_converter.go
95 }
96 > return f, nil failure_converter.go
97 case *nexus.OperationError:
98 if typedErr.OriginalFailure != nil {
123 }
124 return f, nil
125 > default: failure_converter.go
126 > return nexus.Failure{
127 > Message: typedErr.Error(),
128 > }, nil
129 }
130 }
132 // FailureToError implements FailureConverter.
133 // nolint:revive // Keeping all of the logic together for readability, even if it means the function is long.
134 > func (e knownErrorFailureConverter) FailureToError(f nexus.Failure) (error, error) { failure_converter.go
135 > if f.Metadata != nil {
136 switch f.Metadata["type"] {
137 case "nexus.HandlerError":
177 }
178 // Note that the original failure cause is retained on the FailureError's failure object.
179 > fe := &nexus.FailureError{Failure: f} failure_converter.go
180 > if f.Cause != nil {
181 c, err := e.FailureToError(*f.Cause)
182 if err != nil {
196 // [Failure] instances are converted to [FailureError] to allow access to the full failure metadata and details if
197 // available.
198 > func DefaultFailureConverter() FailureConverter { failure_converter.go
199 > return defaultFailureConverter
200 > }
201
202 > func retryBehaviorAsOptionalBool(e *nexus.HandlerError) *bool { failure_converter.go
203 > // nolint:exhaustive // this is a simple optional boolean.
204 > switch e.RetryBehavior {
205 case nexus.HandlerErrorRetryBehaviorRetryable:
206 ret := true
207 return &ret
208 > case nexus.HandlerErrorRetryBehaviorNonRetryable: failure_converter.go
209 > ret := false
210 > return &ret
211 }
212 return nil