Jeremiah Lowin Claude commited on
Commit
80215f2
·
1 Parent(s): 029001b

Fix external schema reference handling in OpenAPI parser

Browse files

Previously, external schema references (URLs) in OpenAPI schemas were
silently passed through and only failed during JSON schema validation
with confusing "failed to match exactly one schema" errors.

This change:
- Detects external references in _replace_ref_with_defs() and raises clear error messages
- Updates exception handlers to propagate external reference errors while preserving other error handling
- Adds comprehensive test coverage for external reference detection
- Provides helpful error messages explaining that FastMCP only supports local schema references

Fixes #926

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

src/fastmcp/utilities/openapi.py CHANGED
@@ -274,6 +274,12 @@ class OpenAPIParser(
274
  result = {}
275
 
276
  return _replace_ref_with_defs(result)
 
 
 
 
 
 
277
  except Exception as e:
278
  logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
279
  return {}
@@ -400,12 +406,30 @@ class OpenAPIParser(
400
  request_body_info.content_schema[media_type_str] = (
401
  schema_dict
402
  )
 
 
 
 
 
 
 
 
 
403
  except Exception as e:
404
  logger.error(
405
  f"Failed to extract schema for media type '{media_type_str}': {e}"
406
  )
407
 
408
  return request_body_info
 
 
 
 
 
 
 
 
 
409
  except Exception as e:
410
  ref_name = getattr(request_body_or_ref, "ref", "unknown")
411
  logger.error(
@@ -449,6 +473,17 @@ class OpenAPIParser(
449
  media_type_obj.media_type_schema
450
  )
451
  resp_info.content_schema[media_type_str] = schema_dict
 
 
 
 
 
 
 
 
 
 
 
452
  except Exception as e:
453
  logger.error(
454
  f"Failed to extract schema for media type '{media_type_str}' "
@@ -456,6 +491,16 @@ class OpenAPIParser(
456
  )
457
 
458
  extracted_responses[str(status_code)] = resp_info
 
 
 
 
 
 
 
 
 
 
459
  except Exception as e:
460
  ref_name = getattr(resp_or_ref, "ref", "unknown")
461
  logger.error(
@@ -556,6 +601,17 @@ class OpenAPIParser(
556
  logger.info(
557
  f"Successfully extracted route: {method_upper} {path_str}"
558
  )
 
 
 
 
 
 
 
 
 
 
 
559
  except Exception as op_error:
560
  op_id = getattr(operation, "operationId", "unknown")
561
  logger.error(
@@ -901,6 +957,12 @@ def _replace_ref_with_defs(
901
  if ref_path.startswith("#/components/schemas/"):
902
  schema_name = ref_path.split("/")[-1]
903
  schema["$ref"] = f"#/$defs/{schema_name}"
 
 
 
 
 
 
904
  elif properties := schema.get("properties"):
905
  if "$ref" in properties:
906
  schema["properties"] = _replace_ref_with_defs(properties)
 
274
  result = {}
275
 
276
  return _replace_ref_with_defs(result)
277
+ except ValueError as e:
278
+ # Re-raise ValueError for external reference errors and other validation issues
279
+ if "External or non-local reference not supported" in str(e):
280
+ raise
281
+ logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
282
+ return {}
283
  except Exception as e:
284
  logger.error(f"Failed to extract schema as dict: {e}", exc_info=False)
285
  return {}
 
406
  request_body_info.content_schema[media_type_str] = (
407
  schema_dict
408
  )
409
+ except ValueError as e:
410
+ # Re-raise ValueError for external reference errors
411
+ if "External or non-local reference not supported" in str(
412
+ e
413
+ ):
414
+ raise
415
+ logger.error(
416
+ f"Failed to extract schema for media type '{media_type_str}': {e}"
417
+ )
418
  except Exception as e:
419
  logger.error(
420
  f"Failed to extract schema for media type '{media_type_str}': {e}"
421
  )
422
 
423
  return request_body_info
424
+ except ValueError as e:
425
+ # Re-raise ValueError for external reference errors
426
+ if "External or non-local reference not supported" in str(e):
427
+ raise
428
+ ref_name = getattr(request_body_or_ref, "ref", "unknown")
429
+ logger.error(
430
+ f"Failed to extract request body '{ref_name}': {e}", exc_info=False
431
+ )
432
+ return None
433
  except Exception as e:
434
  ref_name = getattr(request_body_or_ref, "ref", "unknown")
435
  logger.error(
 
473
  media_type_obj.media_type_schema
474
  )
475
  resp_info.content_schema[media_type_str] = schema_dict
476
+ except ValueError as e:
477
+ # Re-raise ValueError for external reference errors
478
+ if (
479
+ "External or non-local reference not supported"
480
+ in str(e)
481
+ ):
482
+ raise
483
+ logger.error(
484
+ f"Failed to extract schema for media type '{media_type_str}' "
485
+ f"in response {status_code}: {e}"
486
+ )
487
  except Exception as e:
488
  logger.error(
489
  f"Failed to extract schema for media type '{media_type_str}' "
 
491
  )
492
 
493
  extracted_responses[str(status_code)] = resp_info
494
+ except ValueError as e:
495
+ # Re-raise ValueError for external reference errors
496
+ if "External or non-local reference not supported" in str(e):
497
+ raise
498
+ ref_name = getattr(resp_or_ref, "ref", "unknown")
499
+ logger.error(
500
+ f"Failed to extract response for status code {status_code} "
501
+ f"from reference '{ref_name}': {e}",
502
+ exc_info=False,
503
+ )
504
  except Exception as e:
505
  ref_name = getattr(resp_or_ref, "ref", "unknown")
506
  logger.error(
 
601
  logger.info(
602
  f"Successfully extracted route: {method_upper} {path_str}"
603
  )
604
+ except ValueError as op_error:
605
+ # Re-raise ValueError for external reference errors
606
+ if "External or non-local reference not supported" in str(
607
+ op_error
608
+ ):
609
+ raise
610
+ op_id = getattr(operation, "operationId", "unknown")
611
+ logger.error(
612
+ f"Failed to process operation {method_upper} {path_str} (ID: {op_id}): {op_error}",
613
+ exc_info=True,
614
+ )
615
  except Exception as op_error:
616
  op_id = getattr(operation, "operationId", "unknown")
617
  logger.error(
 
957
  if ref_path.startswith("#/components/schemas/"):
958
  schema_name = ref_path.split("/")[-1]
959
  schema["$ref"] = f"#/$defs/{schema_name}"
960
+ elif not ref_path.startswith("#/"):
961
+ raise ValueError(
962
+ f"External or non-local reference not supported: {ref_path}. "
963
+ f"FastMCP only supports local schema references starting with '#/'. "
964
+ f"Please include all schema definitions within the OpenAPI document."
965
+ )
966
  elif properties := schema.get("properties"):
967
  if "$ref" in properties:
968
  schema["properties"] = _replace_ref_with_defs(properties)
tests/utilities/openapi/test_openapi_advanced.py CHANGED
@@ -614,3 +614,52 @@ def test_http_trace_method_path(parsed_http_methods_routes):
614
 
615
  assert trace_route is not None
616
  assert trace_route.path == "/resource"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
 
615
  assert trace_route is not None
616
  assert trace_route.path == "/resource"
617
+
618
+
619
+ @pytest.fixture
620
+ def schema_with_external_reference() -> dict[str, Any]:
621
+ """Fixture that returns a schema with external schema references like in issue #926."""
622
+ return {
623
+ "openapi": "3.0.0",
624
+ "info": {"title": "External Reference API", "version": "1.0.0"},
625
+ "paths": {
626
+ "/products": {
627
+ "post": {
628
+ "summary": "Create a product",
629
+ "operationId": "createProduct",
630
+ "requestBody": {
631
+ "required": True,
632
+ "content": {
633
+ "application/json": {
634
+ "schema": {
635
+ "type": "object",
636
+ "properties": {
637
+ "obj": {
638
+ "$ref": "http://cyaninc.com/json-schemas/market-v1/product-constraints"
639
+ }
640
+ },
641
+ }
642
+ }
643
+ },
644
+ },
645
+ "responses": {"201": {"description": "Product created"}},
646
+ }
647
+ }
648
+ },
649
+ }
650
+
651
+
652
+ # --- Tests for external schema reference handling --- #
653
+
654
+
655
+ def test_external_reference_raises_clear_error(schema_with_external_reference):
656
+ """Test that external schema references raise a clear, helpful error message."""
657
+ with pytest.raises(ValueError) as exc_info:
658
+ parse_openapi_to_http_routes(schema_with_external_reference)
659
+
660
+ error_message = str(exc_info.value)
661
+ assert "External or non-local reference not supported" in error_message
662
+ assert (
663
+ "http://cyaninc.com/json-schemas/market-v1/product-constraints" in error_message
664
+ )
665
+ assert "FastMCP only supports local schema references" in error_message