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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ planned for 2026-01-01

- [weather] feat: add configurable forecast date format option (#3918)
- [core] Add new `server:watch` script to run MagicMirror² server-only with automatic restarts when files (defined in `config.watchTargets`) change (#3920)
- [weather] add error handling to fetch functions including cors (#3791)

### Changed

Expand Down
24 changes: 14 additions & 10 deletions js/server_functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function getStartup (req, res) {
* Only the url-param of the input request url is required. It must be the last parameter.
* @param {Request} req - the request
* @param {Response} res - the result
* @returns {Promise<void>} A promise that resolves when the response is sent
*/
async function cors (req, res) {
try {
Expand All @@ -40,29 +41,32 @@ async function cors (req, res) {
if (!match) {
url = `invalid url: ${req.url}`;
Log.error(url);
res.send(url);
return res.status(400).send(url);
} else {
url = match[1];

const headersToSend = getHeadersToSend(req.url);
const expectedReceivedHeaders = geExpectedReceivedHeaders(req.url);

Log.log(`cors url: ${url}`);
const response = await fetch(url, { headers: headersToSend });

for (const header of expectedReceivedHeaders) {
const headerValue = response.headers.get(header);
if (header) res.set(header, headerValue);
const response = await fetch(url, { headers: headersToSend });
if (response.ok) {
for (const header of expectedReceivedHeaders) {
const headerValue = response.headers.get(header);
if (header) res.set(header, headerValue);
}
const data = await response.text();
res.send(data);
} else {
throw new Error(`Response status: ${response.status}`);
}
const data = await response.text();
res.send(data);
}
} catch (error) {
// Only log errors in non-test environments to keep test output clean
if (process.env.mmTestMode !== "true") {
Log.error(error);
Log.error(`Error in CORS request: ${error}`);
}
res.send(error);
res.status(500).json({ error: error.message });
}
}

Expand Down
30 changes: 20 additions & 10 deletions modules/default/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,29 @@ async function performWebRequest (url, type = "json", useCorsProxy = false, requ
requestUrl = url;
request.headers = getHeadersToSend(requestHeaders);
}
const response = await fetch(requestUrl, request);
const data = await response.text();

if (type === "xml") {
return new DOMParser().parseFromString(data, "text/html");
} else {
if (!data || !data.length > 0) return undefined;
try {
const response = await fetch(requestUrl, request);
if (response.ok) {
const data = await response.text();

if (type === "xml") {
return new DOMParser().parseFromString(data, "text/html");
} else {
if (!data || !data.length > 0) return undefined;

const dataResponse = JSON.parse(data);
if (!dataResponse.headers) {
dataResponse.headers = getHeadersFromResponse(expectedResponseHeaders, response);
const dataResponse = JSON.parse(data);
if (!dataResponse.headers) {
dataResponse.headers = getHeadersFromResponse(expectedResponseHeaders, response);
}
return dataResponse;
}
} else {
throw new Error(`Response status: ${response.status}`);
}
return dataResponse;
} catch (error) {
Log.error(`Error fetching data from ${url}: ${error}`);
return undefined;
}
}

Expand Down
54 changes: 26 additions & 28 deletions modules/default/weather/providers/pirateweather.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,36 @@ WeatherProvider.register("pirateweather", {
lon: 0
},

fetchCurrentWeather () {
this.fetchData(this.getUrl())
.then((data) => {
if (!data || !data.currently || typeof data.currently.temperature === "undefined") {
// No usable data?
return;
}
async fetchCurrentWeather () {
try {
const data = await this.fetchData(this.getUrl());
if (!data || !data.currently || typeof data.currently.temperature === "undefined") {
throw new Error("No usable data received from Pirate Weather API.");
}

const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
})
.catch(function (request) {
Log.error("[weatherprovider.pirateweather] Could not load data ... ", request);
})
.finally(() => this.updateAvailable());
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
} catch (error) {
Log.error("Could not load data ... ", error);
} finally {
this.updateAvailable();
}
},

fetchWeatherForecast () {
this.fetchData(this.getUrl())
.then((data) => {
if (!data || !data.daily || !data.daily.data.length) {
// No usable data?
return;
}
async fetchWeatherForecast () {
try {
const data = await this.fetchData(this.getUrl());
if (!data || !data.daily || !data.daily.data.length) {
throw new Error("No usable data received from Pirate Weather API.");
}

const forecast = this.generateWeatherObjectsFromForecast(data.daily.data);
this.setWeatherForecast(forecast);
})
.catch(function (request) {
Log.error("[weatherprovider.pirateweather] Could not load data ... ", request);
})
.finally(() => this.updateAvailable());
const forecast = this.generateWeatherObjectsFromForecast(data.daily.data);
this.setWeatherForecast(forecast);
} catch (error) {
Log.error("Could not load data ... ", error);
} finally {
this.updateAvailable();
}
},

// Create a URL from the config and base URL.
Expand Down
18 changes: 10 additions & 8 deletions tests/unit/functions/server_functions_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ describe("server_functions tests", () => {
headers: {
get: fetchResponseHeadersGet
},
text: fetchResponseHeadersText
text: fetchResponseHeadersText,
ok: true
};

fetch = vi.fn();
Expand All @@ -26,7 +27,12 @@ describe("server_functions tests", () => {

corsResponse = {
set: vi.fn(() => {}),
send: vi.fn(() => {})
send: vi.fn(() => {}),
status: vi.fn(function (code) {
this.statusCode = code;
return this;
}),
json: vi.fn(() => {})
};

request = {
Expand Down Expand Up @@ -91,15 +97,11 @@ describe("server_functions tests", () => {
throw error;
});

let sentData;
corsResponse.send = vi.fn((input) => {
sentData = input;
});

await cors(request, corsResponse);

expect(fetchResponseHeadersText.mock.calls).toHaveLength(1);
expect(sentData).toBe(error);
expect(corsResponse.status).toHaveBeenCalledWith(500);
expect(corsResponse.json).toHaveBeenCalledWith({ error: error.message });
});

it("Fetches with user agent by default", async () => {
Expand Down