Skip to content

Commit f68d9ee

Browse files
authored
jwk: surface Export type mismatch as KeyTypeMismatchError (#2151)
1 parent f2119e1 commit f68d9ee

4 files changed

Lines changed: 53 additions & 12 deletions

File tree

.claude/docs/error-formatting.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Only `jwt.UnknownPayloadTypeError()` remains a sentinel function (no struct type
4242

4343
| Type | Structured Fields | Meaning |
4444
|------|------------------|---------|
45-
| `KeyTypeMismatchError` | `Got`, `Want` (both `reflect.Type`) | Generic type parameter on `Import[T]` / `ParseKeyAs[T]` does not match the resolved key type |
45+
| `KeyTypeMismatchError` | `Got`, `Want` (both `reflect.Type`) | Generic type parameter on `Import[T]` / `ParseKeyAs[T]` / `Export[T]` / `ExportAll[T]` does not match the value the function produced |
4646
| `UnknownKeyTypeError` | `KeyType` (string; empty when `kty` was missing) | Probe couldn't resolve `kty` to a known key family. Empty `KeyType` = `kty` field absent or non-string; populated = `kty` was a string but unrecognized |
4747
| `FieldNotFoundError` | `Name` | Field not present (`jwk.Get` miss) |
4848
| `FieldTypeMismatchError` | `Name`, `Got`, `Want` | Field present but wrong type (`jwk.Get` type assertion failed) |

jwk/convert.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,10 @@ func Export[T any](key Key) (T, error) {
478478
}
479479
result, ok := v.(T)
480480
if !ok {
481-
return zero, fmt.Errorf(`jwk.Export: exported %T, requested %T`, v, zero)
481+
return zero, fmt.Errorf(`jwk.Export: %w`, KeyTypeMismatchError{
482+
Got: reflect.TypeOf(v),
483+
Want: reflect.TypeFor[T](),
484+
})
482485
}
483486
return result, nil
484487
}

jwk/errors.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,24 @@ func ParseError() error {
9191
// KeyTypeMismatchError
9292
//-------------------------------------------------------------------
9393

94-
// KeyTypeMismatchError is returned by [Import] when the imported key's
95-
// concrete type does not match the generic type parameter supplied by
96-
// the caller.
94+
// KeyTypeMismatchError is returned by [Import] / [ParseKeyAs] /
95+
// [Export] / [ExportAll] when the value the function produced does
96+
// not match the generic type parameter supplied by the caller.
9797
//
98-
// Callers that need to distinguish "wrong generic type parameter" from
99-
// "key validation failed" should use [errors.Is] with
100-
// KeyTypeMismatchError{}, or [errors.AsType] to recover the Got and
101-
// Want fields.
98+
// Got is the runtime type of the value the library produced; Want is
99+
// the type the caller asked for via the type parameter. Callers that
100+
// need to distinguish "wrong generic type parameter" from other
101+
// failures should use [errors.Is] with KeyTypeMismatchError{}, or
102+
// [errors.AsType] to recover the Got and Want fields.
102103
type KeyTypeMismatchError struct {
103-
// Got is the runtime type of the key that was imported.
104+
// Got is the runtime type of the value the library produced.
104105
Got reflect.Type
105-
// Want is the type requested via the Import type parameter.
106+
// Want is the type requested via the function's type parameter.
106107
Want reflect.Type
107108
}
108109

109110
func (e KeyTypeMismatchError) Error() string {
110-
return fmt.Sprintf(`imported key is %s, not %s`, typeName(e.Got), typeName(e.Want))
111+
return fmt.Sprintf(`key type mismatch: got %s, want %s`, typeName(e.Got), typeName(e.Want))
111112
}
112113

113114
func (e KeyTypeMismatchError) Is(target error) bool {

jwk/jwk_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,43 @@ func TestGenericImport(t *testing.T) {
386386
})
387387
}
388388

389+
// TestExportTypeMismatch verifies that jwk.Export and jwk.ExportAll
390+
// surface a type-parameter mismatch as a jwk.KeyTypeMismatchError, the
391+
// same shape jwk.Import and jwk.ParseKeyAs use. errors.As recovers the
392+
// concrete Got/Want types so callers can branch programmatically.
393+
func TestExportTypeMismatch(t *testing.T) {
394+
t.Parallel()
395+
396+
rsaRaw, err := jwxtest.GenerateRsaKey()
397+
require.NoError(t, err)
398+
rsaKey, err := jwk.Import[jwk.Key](rsaRaw)
399+
require.NoError(t, err)
400+
401+
t.Run("Export wrong T", func(t *testing.T) {
402+
t.Parallel()
403+
// rsaKey exports to *rsa.PrivateKey, not *ecdsa.PrivateKey.
404+
_, err := jwk.Export[*ecdsa.PrivateKey](rsaKey)
405+
require.Error(t, err)
406+
require.ErrorIs(t, err, jwk.KeyTypeMismatchError{})
407+
408+
mismatch, ok := errors.AsType[jwk.KeyTypeMismatchError](err)
409+
require.True(t, ok, `errors.AsType should recover KeyTypeMismatchError`)
410+
require.Equal(t, reflect.TypeFor[*rsa.PrivateKey](), mismatch.Got)
411+
require.Equal(t, reflect.TypeFor[*ecdsa.PrivateKey](), mismatch.Want)
412+
})
413+
414+
t.Run("ExportAll wrong T", func(t *testing.T) {
415+
t.Parallel()
416+
set := jwk.NewSet()
417+
require.NoError(t, set.AddKey(rsaKey))
418+
419+
_, err := jwk.ExportAll[*ecdsa.PrivateKey](set)
420+
require.Error(t, err)
421+
require.ErrorIs(t, err, jwk.KeyTypeMismatchError{},
422+
`ExportAll should chain KeyTypeMismatchError through the per-key wrap`)
423+
})
424+
}
425+
389426
func TestExportAll(t *testing.T) {
390427
t.Parallel()
391428

0 commit comments

Comments
 (0)