{"record":{"id":"2ec63f1df1a9407b","repo":"spring-projects/spring-ai","slug":"failed-to-read-media-data-from-url","errorCode":null,"errorMessage":"Failed to read media data from URL: ","messagePattern":"Failed to read media data from URL: ","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java","lineNumber":546,"sourceCode":"\t\telse if (BedrockMediaFormat.isSupportedImageFormat(mimeType)) { // Image\n\t\t\tImageSource.Builder sourceBuilder = ImageSource.builder();\n\t\t\tif (media.getData() instanceof byte[] bytes) {\n\t\t\t\tsourceBuilder.bytes(SdkBytes.fromByteArrayUnsafe(bytes)).build();\n\t\t\t}\n\t\t\telse if (media.getData() instanceof String text) {\n\n\t\t\t\tif (text.startsWith(\"s3://\")) {\n\t\t\t\t\tsourceBuilder.s3Location(S3Location.builder().uri(text).build()).build();\n\t\t\t\t}\n\t\t\t\telse if (text.startsWith(\"http://\") || text.startsWith(\"https://\")) {\n\t\t\t\t\t// Not base64\n\t\t\t\t\tif (URLValidator.isValidURLStrict(text)) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tbyte[] bytes = this.mediaFetcher.fetch(URI.create(text));\n\t\t\t\t\t\t\tsourceBuilder.bytes(SdkBytes.fromByteArrayUnsafe(bytes)).build();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcatch (SecurityException | RestClientException e) {\n\t\t\t\t\t\t\tthrow new RuntimeException(\"Failed to read media data from URL: \" + text, e);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthrow new SecurityException(\"URL is not valid under strict validation rules: \" + text);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// Assume it's base64-encoded image data\n\t\t\t\t\tsourceBuilder.bytes(SdkBytes.fromByteArray(Base64.getDecoder().decode(text)));\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (media.getData() instanceof URL url) {\n\n\t\t\t\ttry {\n\t\t\t\t\tString protocol = url.getProtocol();\n\t\t\t\t\tif (!\"http\".equalsIgnoreCase(protocol) && !\"https\".equalsIgnoreCase(protocol)) {\n\t\t\t\t\t\tthrow new SecurityException(\"Unsupported URL protocol: \" + protocol);\n\t\t\t\t\t}","sourceCodeStart":528,"sourceCodeEnd":564,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java#L528-L564","documentation":"BedrockProxyChatModel.mapMediaToContentBlock wraps SecurityException and RestClientException from fetching a String-typed media URL into this RuntimeException. It means the library attempted an HTTP fetch of the URL passed as Media data and either the fetch failed (network/HTTP error) or a security check rejected it. The failing URL is appended to the message.","triggerScenarios":"Passing a Media whose data is a String that passes strict URL validation (URLValidator.isValidURLStrict) but whose fetch via mediaFetcher.fetch(URI) throws RestClientException (connection refused, DNS failure, non-2xx, timeout), or an inner SecurityException (e.g. blocked scheme or SSRF-guard failure) propagating to the catch.","commonSituations":"Developer passes 'http://localhost:8080/img.png' or an AWS IMDS URL ('http://169.254.169.254/...') as image data; SSRF protections block loopback/link-local hosts. Also common: unreachable hosts, self-signed TLS, or a URL that resolves but returns 404/403 from the media server.","solutions":["Serve the media from a public http/https URL that passes strict validation and is reachable from the client machine.","Prefer base64-encoded data: pass the raw base64 string instead of a URL so no fetch occurs.","If the URL is intentionally local (loopback/IMDS), the strict validator/SSRF guard will block it — embed the bytes instead.","Wrap the call in try-catch for RuntimeException with cause SecurityException|RestClientException to surface the real cause."],"exampleFix":"// before\nMedia media = new Media(MimeTypeUtils.IMAGE_PNG, \"http://localhost:8080/cat.png\");\n// after\nbyte[] bytes = Files.readAllBytes(Path.of(\"cat.png\"));\nMedia media = new Media(MimeTypeUtils.IMAGE_PNG,\n    new Media.DataObject(Base64.getEncoder().encodeToString(bytes)));","handlingStrategy":"try-catch","validationCode":"// Validate before sending\nif (URLValidator.isValidURLStrict(urlString) == false || !(urlString.startsWith(\"http://\") || urlString.startsWith(\"https://\"))) {\n    throw new IllegalArgumentException(\"Media URL not allowed: \" + urlString);\n}","typeGuard":"boolean isFetchableHttpUrl(String s) {\n    return s != null && (s.startsWith(\"http://\") || s.startsWith(\"https://\")) && URLValidator.isValidURLStrict(s);\n}","tryCatchPattern":"try {\n    model.call(prompt);\n} catch (RuntimeException e) {\n    if (e.getCause() instanceof SecurityException || e.getCause() instanceof RestClientException) {\n        // fall back to base64-embedded media\n    } else throw e;\n}","preventionTips":["Prefer base64/byte[] media over remote URLs for local or internal assets","Never point media URLs at localhost, 169.254.169.254, or private ranges","Curl the URL first in CI to verify reachability","Check e.getCause() to distinguish fetch failure from policy rejection"],"tags":["network","ssrf-protection","media-fetch","bedrock"],"backgroundTag":"http-request-failed","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}