14 days ago
Object storage rewrites object bodies based on Content-Type: an XML declaration is prepended to application/xml objects
Summary
Railway object storage modifies the bytes of a stored object when it is written with Content-Type: application/xml: an XML declaration is prepended to the body. A document that already begins with one is returned with two, which makes it unparseable — every XML parser stops at the second declaration with XML declaration allowed only at the start of the document.
GET returns exactly what was PUT when the same bytes are stored under text/plain, so the trigger is the declared media type, not the content.
This is not S3-compatible behaviour. An object store is expected to return the bytes it was given.
Reproduction
Minimal, no application code involved. Credentials and endpoint come from the bucket's own service variables.
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
const c = new S3Client({
region: process.env.S3_REGION, // eu-central-1
endpoint: process.env.S3_ENDPOINT, // https://b1.eu-central-1.storage.railway.app
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
});
const B = process.env.S3_BUCKET_NAME;
const body = '<?xml version="1.0" encoding="UTF-8"?>\n<a/>'; // 43 bytes
async function roundTrip(key, contentType) {
await c.send(new PutObjectCommand({ Bucket: B, Key: key, Body: body, ContentType: contentType }));
const o = await c.send(new GetObjectCommand({ Bucket: B, Key: key }));
const back = await o.Body.transformToString();
console.log(contentType, '| sent', body.length, 'got', back.length, '| identical:', back === body);
}
await roundTrip('probe.xml', 'application/xml');
await roundTrip('probe.txt', 'text/plain');Result
application/xml | sent 43 got 81 | identical: false
text/plain | sent 43 got 43 | identical: trueThe 81-byte body starts:
<?xml version="1.0" encoding="UTF-8"?><?xml version="1.0" encoding="UTF-8"?>\n<a/>HeadObject reports ContentLength: 81, so the object is stored modified rather than being rewritten on read.
Expected
GetObject returns the bytes given to PutObject, byte for byte, whatever the Content-Type.
Impact
Anything served as XML from a Railway bucket is silently corrupted: sitemaps, RSS and Atom feeds, SVG, OPML, XML APIs.
Two properties make this expensive to find:
- The corruption is invisible at write time.
PutObjectsucceeds and returns normally. The damage only appears when something parses the object, which may be hours later and in a different system. - The first symptom is usually not in a log. In our case the visible failure was a browser rendering
XML declaration allowed only at the start of the documenton a public sitemap. Nothing in the application logs was wrong, because nothing in the application was wrong.
A search engine reading a corrupted sitemap gets a parse error, not a partial document.
Environment
- Endpoint:
https://b1.eu-central-1.storage.railway.app, regioneu-central-1 - Client:
@aws-sdk/client-s3(JS v3),forcePathStyle: true - Observed: 2026-08-07
A smaller, related note
S3_PATH_STYLE is injected into the service as a variable, which is helpful — but the AWS SDKs do not read it. A configuration that ignores it produces a confusing error: with virtual-hosted addressing the gateway reads the first path segment as the bucket name, so a request for the key sitemap.xml fails as NoSuchBucket: sitemap.xml, naming the key and never mentioning the real bucket. A line in the storage docs saying the endpoint requires path-style addressing, and that the SDK must be told explicitly, would save the next person the same hunt.
1 Replies
Status changed to Awaiting Railway Response Railway • 14 days ago
14 days ago
Railway Buckets run on Tigris, and the S3 API behavior you are seeing, specifically the content-type-triggered body rewriting, sits in Tigris's service layer, which is not something we control or can patch on our side. For this issue you would need to reach Tigris directly: help@tigrisdata.com or their Discord (#help). On the docs note about path-style addressing, that is fair feedback and we appreciate it.
Status changed to Awaiting User Response Railway • 14 days ago
7 days ago
This thread has been marked as solved automatically due to a lack of recent activity. Please re-open this thread or create a new one if you require further assistance. Thank you!
Status changed to Solved Railway • 7 days ago