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 } : {};
}
Comment on lines +22 to +37

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 response.json() e, se isso falhar, faz fallback para response.text(). Em fetch, o body do Response é um stream que só pode ser consumido uma vez; se json() já leu o body (mesmo lançando por JSON inválido ou body vazio), text() vai falhar com erro de stream já lido. Para tornar o fallback realmente robusto, use response.clone() antes de ler, ou leia text() uma vez e faça JSON.parse condicionalmente (mantendo o texto para a mensagem de erro).

Copilot uses AI. Check for mistakes.

return {};
}

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 aponta para http://jobsglobalscraper.ddns.net/... (HTTP). Isso remove TLS entre a infraestrutura da Vercel e a API e também pode ser bloqueado/alterado por redirecionamentos. Como o código/testes já usam https://jobsglobalscraper.ddns.net/ como base (ex.: testes de VITE_API_BASE_URL), alinhe o rewrite para https://... (ou documente/garanta explicitamente que o backend só serve HTTP).

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

Copilot uses AI. Check for mistakes.
}
]
}
Loading