JS fetch request works from Craft frontend, but not from a different subdomain
Description
A client has an Element API endpoint set up to return quick "as you type" search results. This works beautifully from the Craft site (https://example.com).
The client also has a subdomain powered by HubSpot (https://learn.example.com), and they want the same functionality across both sites. But when I copy over the same exact JS, the returned response is supposedly a 404, even though the JSON payload is identical.
Any ideas on why it's not working from a subdomain? I do have Access-Control-Allow-Origin defined properly, and I do see the difference it makes when it's there vs not there...
element-api.php
<?php
use craft\elements\Entry;
return
[
'endpoints' =>
[
'api/search' => function()
{
Craft::$app->getResponse()->getHeaders()->set('Access-Control-Allow-Origin', 'https://learn.example.com');
Craft::$app->getResponse()->getHeaders()->set('Access-Control-Allow-Credentials', true);
// settings
$section_handle = 'about, blog, caseStudies, industries, services, work';
$phrase = Craft::$app->request->getParam('query');
$criteria = [
'section' => $section_handle,
'limit' => 5,
'orderBy' => 'score',
'search' => $phrase,
];
return [
'elementType' => Entry::class,
'criteria' => $criteria,
'paginate' => false,
'transformer' => function(craft\elements\Entry $entry)
{
return [
'title' => $entry->title,
'url' => $entry->section->handle == 'blog' ? $entry->permalink : $entry->url,
'section' => $entry->section->name,
];
},
];
},
]
];
JS search functionality (on the HubSpot subdomain site):
function delay(fn, ms)
{
let timer = 0
return function(...args)
{
clearTimeout(timer)
timer = setTimeout(fn.bind(this, ...args), ms || 0)
}
}
let searchAutocompleteInputs = document.querySelectorAll('.jsSearchAutocomplete');
searchAutocompleteInputs.forEach(input =>
{
let autocompleteId = input.dataset.list;
let autocomplete = document.getElementById(autocompleteId);
input.addEventListener('input', delay( function(e)
{
let keyword = e.target.value;
if (keyword)
{
fetch('https://example.com/api/search' + `?query=${keyword}`, {mode: 'cors'})
.then((response) =>
{
console.log({response});
if (response.ok)
{
return response.json();
}
throw new Error('Something went wrong with the fetch');
})
.then( (responseJson) =>
{
// blah blah...
})
.catch((error) =>
{
console.warn(error);
});
}
// Empty search field
else
{
autocomplete.innerHTML = '';
}
}, 500));
});
"404" response from Hubspot site:
# General
- Request URL: https://example.com/api/search?query=logo
- Request Method: GET
- Status Code: 404
- Remote Address: [redacted]:443
- Referrer Policy: no-referrer-when-downgrade
# Response Headers
- Access-Control-Allow-Credentials: 1
- Access-Control-Allow-Origin: https://learn.example.com
- Cf-Cache-Status: DYNAMIC
- Cf-Ray: 7f43074238c72bca-FRA
- Content-Encoding: br
- Content-Type: application/json; charset=UTF-8
- Date: Wed, 09 Aug 2023 21:17:52 GMT
- Link: <https://example.com/api/search>; rel="canonical"
- Nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
- Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=blahblahblah"}],"group":"cf-nel","max_age":604800}
- Server: cloudflare
- Vary: Accept-Encoding
- X-Powered-By: Craft CMS
- X-Robots-Tag: none
# Request Headers
- :authority: example.com
- :method: GET
- :path: /api/search?query=logo
- :scheme: https
- Accept: */*
- Accept-Encoding: gzip, deflate, br
- Accept-Language: en-US,en;q=0.9,he;q=0.8,ar;q=0.7
- Origin: https://learn.example.com
- Referer: https://learn.example.com/?hsDebug=true
- Sec-Ch-Ua: "Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"
- Sec-Ch-Ua-Mobile: ?0
- Sec-Ch-Ua-Platform: "Windows"
- Sec-Fetch-Dest: empty
- Sec-Fetch-Mode: cors
- Sec-Fetch-Site: same-site
- User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
Valid response from the Craft site:
# General
- Request URL: https://example.com/api/search?query=logo
- Request Method: GET
- Status Code: 200
- Remote Address: [redacted]:443
- Referrer Policy: strict-origin-when-cross-origin
# Response Headers
- Access-Control-Allow-Credentials: 1
- Access-Control-Allow-Origin: https://learn.example.com
- Cache-Control: no-store, no-cache, must-revalidate
- Cf-Cache-Status: DYNAMIC
- Cf-Ray: 7f4301de1ad82bc1-FRA
- Content-Encoding: br
- Content-Type: application/json; charset=UTF-8
- Date: Wed, 09 Aug 2023 21:14:11 GMT
- Expires: Thu, 19 Nov 1981 08:52:00 GMT
- Link: <https://example.com/api/search>; rel="canonical"
- Nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
- Pragma: no-cache
- Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=blahblah"}],"group":"cf-nel","max_age":604800}
- Server: cloudflare
- Set-Cookie: blahblah expires=Wed, 23-Aug-2023 21:14:11 GMT; Max-Age=1209600; path=/; secure; HttpOnly
- Vary: Accept-Encoding
- X-Debug-Duration: 211
- X-Debug-Link: https://example.com/actions/debug/default/view?tag=64d401a3b341e
- X-Debug-Tag: 64d401a3b341e
- X-Powered-By: Craft CMS
- X-Robots-Tag: none
# Request Headers
- :authority: example.com
- :method: GET
- :path: /api/search?query=logo
- :scheme: https
- Accept: */*
- Accept-Encoding: gzip, deflate, br
- Accept-Language: en-US,en;q=0.9,he;q=0.8,ar;q=0.7
- Cookie: blahblah
- Referer: https://example.com/
- Sec-Ch-Ua: "Not/A)Brand";v="99", "Google Chrome";v="115", "Chromium";v="115"
- Sec-Ch-Ua-Mobile: ?0
- Sec-Ch-Ua-Platform: "Windows"
- Sec-Fetch-Dest: empty
- Sec-Fetch-Mode: cors
- Sec-Fetch-Site: same-origin
- User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36
Additional info
- Craft version: Craft CMS 3.7.55.2
- PHP version: 7.4.33
- Database driver & version: MySQL 8.0.30
- Plugins & versions:
- Advanced Image Field 1.0.0
- Button Box 3.1.1
- Contact Form 2.5.1
- Contact Form Honeypot 1.0.3
- Control Panel CSS 2.4.0
- Digital Download 2.1.9
- DigitalOcean Spaces Volume 1.1.3
- Element API 2.8.6.1
- Feed Me 4.6.3.1
- Field Manager 2.2.5
- Forms 3.13.20
- Guest Entries 2.4.0
- Imager X v3.6.5
- Imager X Storage Driver for DigitalOcean Spaces 2.0.0
- Inventory 2.1.1
- [Custom Plugin]1.0.0
- Mix 1.5.2
- Neo 2.13.15
- oEmbed 1.3.18
- Postmark 2.1.0
- Redactor 2.10.10
- Redactor Custom Styles 3.0.4
- Redirects 3.2.10
- Scout 2.7.2
- SEO 3.7.4
- Sprout Email 4.4.10
- Super Table 2.7.3
- Typed link field 1.0.25
Bump! I'm still experiencing this issue, now under Craft 4. Any takers?
@brandonkelly Ping! 🙂
Just tested this locally, and as long as I have access-control-allow-origin: * in the response headers, it’s working fine on my end.
As of Craft 5.3, you can configure Craft to send CORS headers by adding the following to config/app.php (at the top level of the return array):
return [
// ...
'as corsFilter' => craft\filters\Cors::class,
];
Or if you want to specify certain allowed origins:
return [
// ...
'as corsFilter' => [
'class' => craft\filters\Cors::class,
'cors' => [
'Origin' => [
'https://learn.example.com',
],
'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
'Access-Control-Request-Headers' => ['*'],
'Access-Control-Allow-Credentials' => true,
'Access-Control-Max-Age' => 86400,
'Access-Control-Expose-Headers' => [],
],
],
];