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
10 changes: 8 additions & 2 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,15 +593,21 @@ export class RunCommand extends ApifyCommand<typeof RunCommand> {

if (mime.getExtension(existingInput.contentType!) === 'json') {
// Step 4: validate the input
const inputJson = JSON.parse(existingInput.body.toString('utf-8'));
let inputJson: unknown;

try {
inputJson = JSON.parse(existingInput.body.toString('utf-8'));
} catch (err) {
throw new Error(`Cannot parse JSON input file at path "${inputFilePath}".\n ${(err as Error).message}`);
}

if (Array.isArray(inputJson)) {
throw new Error('The input in your storage is invalid. It should be an object, not an array.');
}

const fullInput = {
...defaults,
...inputJson,
...(inputJson as Record<string, unknown>),
};

const errors = validateInputUsingValidator(compiledInputSchema, inputSchema, fullInput);
Expand Down
42 changes: 31 additions & 11 deletions src/lib/commands/resolve-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import mime from 'mime';
import { cachedStdinInput } from '../../entrypoints/_shared.js';
import { CommandExitCodes } from '../consts.js';
import { error } from '../outputs.js';
import { getLocalInput } from '../utils.js';
import { getLocalInput, getLocalKeyValueStorePath } from '../utils.js';

interface InputOverrideOptions {
schemaHint?: string;
Expand All @@ -31,7 +31,12 @@ export function resolveInput(cwd: string, inputOverride: Record<string, unknown>
const ext = mime.getExtension(localInput.contentType!);

if (ext === 'json') {
inputToUse = JSON.parse(localInput.body.toString('utf8'));
try {
inputToUse = JSON.parse(localInput.body.toString('utf8'));
} catch (err) {
const filePath = path.join(cwd, getLocalKeyValueStorePath(), localInput.fileName!);
throw new Error(`Cannot parse JSON input file at path "${filePath}".\n ${(err as Error).message}`);
}
contentType = 'application/json';
} else {
inputToUse = localInput.body as never;
Expand Down Expand Up @@ -165,26 +170,41 @@ export async function getInputOverride(
// Try reading the file, and if that fails, try reading it as JSON

let fsError: unknown;
let fileContent: string | undefined;

try {
const fileContent = await readFile(fullPath, 'utf8');
const parsed = JSON.parse(fileContent);
fileContent = await readFile(fullPath, 'utf8');
} catch (err) {
fsError = err;
}

if (Array.isArray(parsed)) {
if (fileContent !== undefined) {
try {
const parsed = JSON.parse(fileContent);

if (Array.isArray(parsed)) {
error({
message: withSchemaHint(
'The provided input is invalid. It should be an object, not an array.',
schemaHint,
),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}

input = parsed;
source = inputFileFlag;
} catch (err) {
error({
message: withSchemaHint(
'The provided input is invalid. It should be an object, not an array.',
`Cannot parse JSON input file at path "${fullPath}".\n ${(err as Error).message}`,
schemaHint,
),
});
process.exitCode = CommandExitCodes.InvalidInput;
return false;
}

input = parsed;
source = inputFileFlag;
} catch (err) {
fsError = err;
}

if (fsError) {
Expand Down
14 changes: 13 additions & 1 deletion test/local/commands/run.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path/win32';
import { dirname } from 'node:path';

import { ACTOR_ENV_VARS, APIFY_ENV_VARS } from '@apify/consts';

Expand Down Expand Up @@ -306,6 +306,18 @@ writeFileSync(String.raw\`${joinPath('result.txt')}\`, 'hello world');
expect(lastErrorMessage()).toMatch(/Field awesome must be boolean/i);
});

it('throws with the input file path when stored input JSON is malformed', async () => {
writeFileSync(inputPath, '{"awesome": ', { flag: 'w' });
copyFileSync(defaultsInputSchemaPath, inputSchemaPath);

await testRunCommand(RunCommand, {});

const stderr = lastErrorMessage();
expect(stderr).toContain('Cannot parse JSON input file at path');
expect(stderr).toContain('INPUT.json');
expect(stderr).toContain('Unexpected end of JSON input');
});

it('throws when passing manual input, but local file has correct input', async () => {
writeFileSync(inputPath, '{"awesome": true}', { flag: 'w' });
copyFileSync(defaultsInputSchemaPath, inputSchemaPath);
Expand Down
44 changes: 42 additions & 2 deletions test/local/lib/resolve-input.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import process from 'node:process';

import { afterEach, describe, expect, it } from 'vitest';

import { getInputOverride } from '../../../src/lib/commands/resolve-input.js';
import { getInputOverride, resolveInput } from '../../../src/lib/commands/resolve-input.js';
import { CommandExitCodes } from '../../../src/lib/consts.js';
import { getLocalKeyValueStorePath } from '../../../src/lib/utils.js';
import { useConsoleSpy } from '../../__setup__/hooks/useConsoleSpy.js';

const SCHEMA_HINT = 'Run "apify actors info apify/hello-world --input" to inspect the Actor input schema.';

const { logMessages } = useConsoleSpy();
const tempDirs: string[] = [];

describe('getInputOverride', () => {
afterEach(() => {
afterEach(async () => {
process.exitCode = undefined;

await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});

it('does not append schema hint to --input file path errors', async () => {
Expand Down Expand Up @@ -52,4 +59,37 @@ describe('getInputOverride', () => {
expect(stderr).toContain('It should be an object, not an array.');
expect(stderr).toContain(SCHEMA_HINT);
});

it('includes the file path when --input-file contains malformed JSON', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'apify-cli-input-'));
tempDirs.push(tempDir);
const inputPath = join(tempDir, 'bad-input.json');
await writeFile(inputPath, '{"url":');

const result = await getInputOverride(tempDir, undefined, 'bad-input.json');

expect(result).toBe(false);
expect(process.exitCode).toBe(CommandExitCodes.InvalidInput);
const stderr = logMessages.error.join('\n');
expect(stderr).toContain(`Cannot parse JSON input file at path "${resolve(tempDir, 'bad-input.json')}".`);
expect(stderr).toContain('Unexpected end of JSON input');
});
});

describe('resolveInput', () => {
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});

it('includes the file path when stored INPUT.json contains malformed JSON', async () => {
const tempDir = await mkdtemp(join(tmpdir(), 'apify-cli-input-'));
tempDirs.push(tempDir);
const kvStorePath = join(tempDir, getLocalKeyValueStorePath());
await mkdir(kvStorePath, { recursive: true });
await writeFile(join(kvStorePath, 'INPUT.json'), '{"url":');

expect(() => resolveInput(tempDir, undefined)).toThrow(
`Cannot parse JSON input file at path "${join(kvStorePath, 'INPUT.json')}".`,
);
});
});