Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/loader-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export {

// REQUEST UTILS
export {default as RequestScheduler} from './lib/request-utils/request-scheduler';
export {parseContentType} from './lib/request-utils/parse-content-type';

// PATH HELPERS
export {setPathPrefix, getPathPrefix, resolvePath} from './lib/path-utils/file-aliases';
Expand Down
22 changes: 22 additions & 0 deletions modules/loader-utils/src/lib/request-utils/parse-content-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

/**
* Normalize a Content-Type header into a lowercased media type string.
* @param contentTypeHeader - Raw Content-Type header value.
* @returns Lowercased media type, or null when not available.
*/
export function parseContentType(contentTypeHeader?: string | null): string | null {
if (!contentTypeHeader) {
return null;
}

const trimmedValue = contentTypeHeader.trim();
if (!trimmedValue) {
return null;
}

const contentType = trimmedValue.split(';')[0]?.trim().toLowerCase();
return contentType || null;
}
1 change: 1 addition & 0 deletions modules/loader-utils/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import './lib/path-utils/file-aliases.spec';
import './lib/path-utils/path.spec';

import './lib/request-utils/request-scheduler.spec';
import './lib/request-utils/parse-content-type.spec';
import './lib/javascript-utils/is-type.spec';

// import './lib/files/node-file-facade.spec';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import test from 'tape-promise/tape';
import {parseContentType} from '@loaders.gl/loader-utils';

test('parseContentType', (t) => {
t.equal(parseContentType(null), null, 'returns null for null header');
t.equal(parseContentType(''), null, 'returns null for empty header');
t.equal(
parseContentType('text/html; charset=utf-8'),
'text/html',
'strips charset'
);
t.equal(parseContentType(' Application/JSON '), 'application/json', 'normalizes case');
t.end();
});
19 changes: 18 additions & 1 deletion modules/mvt/src/mvt-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import type {
ImageTileSource,
VectorTileSource,
GetTileParameters,
GetTileDataParameters
GetTileDataParameters,
parseContentType
} from '@loaders.gl/loader-utils';
Comment on lines 10 to 14
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Import parseContentType as a value, not type-only

Because parseContentType is imported via a type-only import, it is erased from the JS output. When getTile executes, parseContentType is not defined and a ReferenceError is thrown before any tile parsing can happen. This will break all runtime tile fetching paths that call MVTTileSource.getTile, regardless of the response headers. Import parseContentType as a value (or add a separate non-type import) so it is available at runtime.

Useful? React with 👍 / 👎.

import {DataSource} from '@loaders.gl/loader-utils';
import {ImageLoader, ImageLoaderOptions, getBinaryImageMetadata} from '@loaders.gl/images';
Expand Down Expand Up @@ -131,6 +132,22 @@ export class MVTTileSource
if (!response.ok) {
return null;
}
const contentType = parseContentType(response.headers.get('content-type'));
if (contentType) {
const isMvtContentType = MVTFormat.mimeTypes.includes(contentType);
const isImageContentType = contentType.startsWith('image/');
const isOctetStreamContentType = contentType === 'application/octet-stream';
const isKnownNonMvtType =
contentType.startsWith('text/') ||
contentType === 'application/json' ||
contentType === 'application/xml';
if (
isKnownNonMvtType ||
(!isMvtContentType && !isImageContentType && !isOctetStreamContentType)
) {
return null;
}
}
const arrayBuffer = await response.arrayBuffer();
return arrayBuffer;
}
Expand Down
33 changes: 32 additions & 1 deletion modules/mvt/test/mvt-source.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import test from 'tape-promise/tape';
import {isBrowser} from '@loaders.gl/core';

import {TILESETS} from './data/tilesets';
import {MVTSource} from '@loaders.gl/mvt';
import {MVTLoader, MVTSource} from '@loaders.gl/mvt';
import {isURLTemplate, getURLFromTemplate} from '../src/mvt-source';
import {MVTTileSource} from '../src/mvt-source';

test('MVTSource#urls', async (t) => {
if (!isBrowser) {
Expand Down Expand Up @@ -86,6 +87,36 @@ test('getURLFromTemplate', (t) => {
t.end();
});

test('MVTTileSource#getTileData returns null for text/html responses', async (t) => {
let didCallParse = false;
const originalParse = MVTLoader.parse;
MVTLoader.parse = async () => {
didCallParse = true;
return {};
};

const source = new MVTTileSource('https://example.com/{z}/{x}/{y}.pbf', {
core: {
loadOptions: {
fetch: async () =>
new Response('<html></html>', {
status: 200,
headers: {'content-type': 'text/html'}
})
}
}
});

try {
const tileData = await source.getTileData({index: {x: 0, y: 0, z: 0}});
t.equal(tileData, null, 'returns null for non-MVT response');
t.equal(didCallParse, false, 'does not invoke MVTLoader.parse');
} finally {
MVTLoader.parse = originalParse;
}
t.end();
});

// TBA - TILE LOADING TESTS

/*
Expand Down
Loading