-
Notifications
You must be signed in to change notification settings - Fork 10
fix: JSONL files not opening in preview (ASTD-261) #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
marcusds
merged 6 commits into
main
from
astd-261-bug-investigate-why-jsonl-file-is-not-opening-in-preview/mschwab
Jun 29, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
10949db
fix: add known text extension allowlist to file binary detection
marcusds 80bb15b
refactor(studio): remove HEAD request from useIsBinaryFile since back…
marcusds e2cfa66
fix(studio): update stale doc comment in useIsBinaryFile
marcusds 34e8618
refactor(studio): simplify useIsBinaryFile signature to accept only f…
marcusds b63528b
refactor(studio): move KNOWN_TEXT_EXTENSIONS to constants file
marcusds e4254fe
test(studio): test .yaml and .yml extensions separately in useIsBinar…
marcusds File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
web/packages/studio/src/components/filesets/hooks/useIsBinaryFile.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { useIsBinaryFile } from '@studio/components/filesets/hooks/useIsBinaryFile'; | ||
| import { renderHook } from '@testing-library/react'; | ||
|
|
||
| describe('useIsBinaryFile', () => { | ||
| it('returns isBinary=false for .jsonl files', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('data/test.jsonl')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=false for .json files', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('config.json')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=false for .csv files', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('data.csv')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=false for .py files', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('script.py')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=false for .yaml and .yml files', () => { | ||
| const { result: yamlResult } = renderHook(() => useIsBinaryFile('config.yaml')); | ||
| const { result: ymlResult } = renderHook(() => useIsBinaryFile('config.yml')); | ||
|
|
||
| expect(yamlResult.current).toEqual({ isBinary: false, isLoading: false }); | ||
| expect(ymlResult.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=false for .md files', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('README.md')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=false when filePath is undefined', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile(undefined)); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=true for .png files (binary blocklist)', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('image.png')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: true, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=true for .zip files (binary blocklist)', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('archive.zip')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: true, isLoading: false }); | ||
| }); | ||
|
|
||
| it('returns isBinary=false for unknown extensions (fail-open)', () => { | ||
| const { result } = renderHook(() => useIsBinaryFile('data.unknown')); | ||
|
|
||
| expect(result.current).toEqual({ isBinary: false, isLoading: false }); | ||
| }); | ||
| }); |
86 changes: 19 additions & 67 deletions
86
web/packages/studio/src/components/filesets/hooks/useIsBinaryFile.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,79 +1,31 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| // Importing from the SDK fetchers module activates the Axios interceptor that | ||
| // injects the OIDC Bearer token, so axios.head() calls below are auth-aware. | ||
| import '@nemo/sdk/generated/fetchers/platform'; | ||
| import { getFilesDownloadFileQueryKey } from '@nemo/sdk/generated/platform/api'; | ||
| import { KNOWN_TEXT_EXTENSIONS } from '@studio/constants/constants'; | ||
| import { isBinaryExtension } from '@studio/util/binaryFile'; | ||
| import { useQuery } from '@tanstack/react-query'; | ||
| import axios from 'axios'; | ||
|
|
||
| function isKnownTextExtension(path: string): boolean { | ||
| const ext = path.split('.').at(-1)?.toLowerCase(); | ||
| return ext !== undefined && KNOWN_TEXT_EXTENSIONS.has(ext); | ||
| } | ||
|
|
||
| /** | ||
| * Determine whether a fileset file should be treated as binary (no text preview). | ||
| * | ||
| * Strategy (three tiers): | ||
| * 1. Extension in `BINARY_FILE_EXTENSIONS` → binary immediately, no network. | ||
| * 2. Extension not in blocklist → HEAD request; `Content-Type` header decides. | ||
| * - `text/*` or known text application types → not binary. | ||
| * - Everything else → binary. | ||
| * 3. HEAD fails / no Content-Type → assume text (fail-open for preview). | ||
| * Strategy: | ||
| * - Extension in `KNOWN_TEXT_EXTENSIONS` → text immediately. | ||
| * - Extension in `BINARY_FILE_EXTENSIONS` → binary immediately. | ||
| * - Unknown extension → assume text (fail-open for preview). | ||
| * | ||
| * Returns `{ isBinary, isLoading }`. `isLoading` is true only during the HEAD | ||
| * request (tier-2 path); tier-1 resolves synchronously. | ||
| * Returns `{ isBinary, isLoading }`. `isLoading` is always `false` since | ||
| * detection is synchronous. | ||
| */ | ||
| export function useIsBinaryFile( | ||
| workspace: string, | ||
| filesetName: string, | ||
| filePath: string | undefined | ||
| ): { isBinary: boolean; isLoading: boolean } { | ||
| const blocklisted = filePath !== undefined && isBinaryExtension(filePath); | ||
|
|
||
| const { data: headBinary, isPending } = useQuery({ | ||
| queryKey: ['file-content-type', workspace, filesetName, filePath], | ||
| queryFn: async (): Promise<boolean> => { | ||
| if (!filePath) return false; | ||
| try { | ||
| // axios.head() is auth-aware via the interceptor registered when | ||
| // '@nemo/sdk/generated/fetchers/platform' is imported above. | ||
| const [fileUrl] = getFilesDownloadFileQueryKey( | ||
| encodeURIComponent(workspace), | ||
| encodeURIComponent(filesetName), | ||
| encodeURIComponent(filePath) | ||
| ); | ||
| const res = await axios.head(fileUrl); | ||
| const ct = String(res.headers['content-type'] ?? ''); | ||
| return !isTextContentType(ct); | ||
| } catch { | ||
| return false; // fail-open: assume text | ||
| } | ||
| }, | ||
| enabled: !!filePath && !blocklisted, | ||
| staleTime: Infinity, | ||
| retry: false, | ||
| }); | ||
|
|
||
| if (blocklisted) return { isBinary: true, isLoading: false }; | ||
| export function useIsBinaryFile(filePath: string | undefined): { | ||
| isBinary: boolean; | ||
| isLoading: boolean; | ||
| } { | ||
| if (!filePath) return { isBinary: false, isLoading: false }; | ||
| return { isBinary: headBinary ?? false, isLoading: isPending }; | ||
| } | ||
|
|
||
| const TEXT_CONTENT_TYPES = [ | ||
| 'text/', | ||
| 'application/json', | ||
| 'application/xml', | ||
| 'application/javascript', | ||
| 'application/typescript', | ||
| 'application/yaml', | ||
| 'application/x-yaml', | ||
| 'application/toml', | ||
| 'application/csv', | ||
| 'application/x-sh', | ||
| ]; | ||
|
|
||
| function isTextContentType(ct: string): boolean { | ||
| // Extract the MIME type token only (strip "; charset=..." parameters) before | ||
| // matching, so parameter values can't accidentally trigger a false positive. | ||
| const mimeToken = ct.split(';')[0].trim().toLowerCase(); | ||
| return TEXT_CONTENT_TYPES.some((prefix) => mimeToken.startsWith(prefix)); | ||
| if (isKnownTextExtension(filePath)) return { isBinary: false, isLoading: false }; | ||
| if (isBinaryExtension(filePath)) return { isBinary: true, isLoading: false }; | ||
| return { isBinary: false, isLoading: false }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.