Skip to content
Merged
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
30 changes: 25 additions & 5 deletions frontend/src/services/jobsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,26 @@ function buildApiUrl(path: string): string {
return baseUrl ? `${baseUrl}${normalizedPath}` : normalizedPath;
}

async function readPayload(response: Response): Promise<Record<string, unknown>> {
const contentType = response.headers?.get?.("content-type") ?? "";

if (!contentType || contentType.includes("application/json")) {
try {
const payload = await response.json();
return payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
} catch {
// Fallback below for non-JSON bodies returned by proxies/platforms.
}
}

if (typeof response.text === "function") {
const text = await response.text();
return text ? { message: text } : {};
}

return {};
Comment on lines +25 to +39

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

readPayload tenta fazer response.json() quando o content-type está vazio e, se o parse falhar, faz fallback para response.text(). Em fetch, ler o body via json() consome o stream; após uma exceção de parse normalmente não é possível ler o body novamente, então o fallback tende a não funcionar. Sugestão: ler o body uma única vez via text() e então fazer JSON.parse quando apropriado, ou usar response.clone() antes de tentar json() para preservar o body para o fallback.

Suggested change
if (!contentType || contentType.includes("application/json")) {
try {
const payload = await response.json();
return payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
} catch {
// Fallback below for non-JSON bodies returned by proxies/platforms.
}
}
if (typeof response.text === "function") {
const text = await response.text();
return text ? { message: text } : {};
}
return {};
if (typeof response.text !== "function") {
return {};
}
const text = await response.text();
if (!text) {
return {};
}
if (!contentType || contentType.includes("application/json")) {
try {
const payload = JSON.parse(text);
return payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
} catch {
// Fallback below for non-JSON bodies returned by proxies/platforms.
}
}
return { message: text };

Copilot uses AI. Check for mistakes.
}

function buildError(message: unknown, fallback: string): Error {
return new Error(typeof message === "string" && message ? message : fallback);
}
Expand All @@ -35,7 +55,7 @@ function readMessage(payload: unknown): string | undefined {

export async function fetchJobFiles(): Promise<JobFile[]> {
const response = await fetch(buildApiUrl("/api/jobs/files"));
const payload = (await response.json()) as { files?: unknown } & Record<string, unknown>;
const payload = (await readPayload(response)) as { files?: unknown } & Record<string, unknown>;

if (!response.ok) {
throw buildError(readMessage(payload), "Falha ao listar arquivos de vagas.");
Expand All @@ -54,7 +74,7 @@ export async function fetchJobFiles(): Promise<JobFile[]> {
export async function fetchJobsByFile(fileName: string): Promise<JobsResponse> {
const suffix = fileName ? `?file=${encodeURIComponent(fileName)}` : "";
const response = await fetch(buildApiUrl(`/api/jobs${suffix}`));
const payload = (await response.json()) as Record<string, unknown>;
const payload = (await readPayload(response)) as Record<string, unknown>;

if (!response.ok) {
throw buildError(readMessage(payload), "Falha ao carregar vagas.");
Expand All @@ -70,7 +90,7 @@ export async function fetchJobsByFile(fileName: string): Promise<JobsResponse> {

export async function fetchKeywords(): Promise<string[]> {
const response = await fetch(buildApiUrl("/api/keywords"));
const payload = (await response.json()) as { keywords?: unknown } & Record<string, unknown>;
const payload = (await readPayload(response)) as { keywords?: unknown } & Record<string, unknown>;

if (!response.ok) {
throw buildError(readMessage(payload), "Falha ao carregar keywords.");
Expand All @@ -87,7 +107,7 @@ export async function saveKeywords(keywords: string[]): Promise<void> {
},
body: JSON.stringify({ keywords }),
});
const payload = (await response.json()) as Record<string, unknown>;
const payload = (await readPayload(response)) as Record<string, unknown>;

if (!response.ok) {
throw buildError(readMessage(payload), "Falha ao salvar keywords.");
Expand All @@ -98,7 +118,7 @@ export async function runScraperRequest(): Promise<void> {
const response = await fetch(buildApiUrl("/api/scraper/run"), {
method: "POST",
});
const payload = (await response.json()) as Record<string, unknown>;
const payload = (await readPayload(response)) as Record<string, unknown>;

if (!response.ok) {
throw buildError(readMessage(payload), "Falha ao executar o scraper.");
Expand Down
13 changes: 13 additions & 0 deletions frontend/tests/unit/utils/jobsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,19 @@ describe("jobsService", () => {
await expect(fetchKeywords()).rejects.toThrow("erro-api");
});

it("lanca erro legivel quando a resposta nao e JSON", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
ok: false,
headers: { get: () => "text/plain; charset=utf-8" },
text: async () => "Vercel rewrite nao encontrado.",
})),
);

await expect(fetchKeywords()).rejects.toThrow("Vercel rewrite nao encontrado.");
});

it("salva keywords com sucesso", async () => {
const fetchMock = vi.fn(async () => ({
ok: true,
Expand Down
8 changes: 8 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"rewrites": [
{
"source": "/api/(.*)",
"destination": "http://jobsglobalscraper.ddns.net/api/$1"

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

O rewrite está apontando para um destino http:// (sem TLS). Isso expõe tráfego em texto claro entre a infraestrutura da Vercel e o backend; se o backend suporta HTTPS, prefira https://... (ou outra solução de proxy seguro).

Suggested change
"destination": "http://jobsglobalscraper.ddns.net/api/$1"
"destination": "https://jobsglobalscraper.ddns.net/api/$1"

Copilot uses AI. Check for mistakes.
}
]
}
Comment on lines +1 to +8

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

Já existe um frontend/vercel.json com o mesmo conteúdo. Manter duas configs de Vercel em locais diferentes tende a causar drift/confusão sobre qual arquivo é efetivamente usado (depende do Root Directory configurado no projeto Vercel). Sugestão: consolidar em um único local e remover o duplicado, ou documentar explicitamente qual diretório é o root do deploy.

Copilot uses AI. Check for mistakes.
Loading