A bit of a background, I’ve recently been working on a reasonably large, headless, multilingual WordPress build. The frontend and the backend needed to be hosted on separate domains, so we decided to build this site using the following technologies:
Backend:
- WordPress via the REST API
- JWT Authentication for the API
- WPML for i18n
- ACF Pro for custom fields.
Frontend:
All was going well until we were told late on in the project that we needed to support IE11. After polyfilling React and any custom libraries we’d introduced for the build, there was one real issue remaining, our ACF options pages weren’t translating in IE.
To talk you through this, I’ll need to rewind a little.
ACF Options Pages with WPML
ACF in all it’s greatness, doesn’t work amazingly with WPML. Posts, pages, media etc. is all fine, but if you use their options pages, they don’t get translated correctly (probably to do with them not being a post type).
To counter this, I found a little hack I could use when registering the page. When calling the acf_add_options_page()
function to register a new options page, you can pass through an array of arguments, one of these being post_id
. The post_id
is what the options page will be saved under in the wp_options
table.
I realised that if I adjusted this ID to append the current language to the post_id
field, I’d be able to query options by language. As such, I wrote the following piece of code to register the pages using WPML.
if ( function_exists( 'acf_add_options_page' ) ) { // Default page title $options = array( 'page_title' => 'Message', ); // If WPML is active, append the language code to the post_id if ( defined( 'ICL_LANGUAGE_CODE' ) ) { $options['post_id'] = sprintf( 'options_%s', ICL_LANGUAGE_CODE ); } // Register the options page acf_add_options_page( $options ); }
I then had a custom WP REST endpoint set up to query these options. The method that did most of the work, looks something like this:
** * The callback for the `wp/v2/acf/options` endpoint * * @param WP_REST_Request $request The WP_REST_Request object * * @return array|string The single requested option, or all options * * @see ACFtoWPAPI::addACFOptionRouteV2() * * @since 1.3.0 */ function addACFOptionRouteV2cb( WP_REST_Request $request ) { $options_string = 'options'; $wpml_lang = $request->get_header( 'X-WP-Wpml-Language' ); if ( $wpml_lang ) { $options_string = sprintf( 'options_%s', $wpml_lang ); } if ( $request['option'] ) { return get_field( $request['option'], $options_string ); } return get_fields( $options_string ); }
As you can see, the method is using a custom HTTP header called X-WP-Wpml-Language
to get the current language from the React side.
Allowing Custom Headers through CORS Preflight
To allow our custom header, we need to OK it with the REST API so that CORS Preflights let React know that it’s an allowed header.
After reading around about how to do this, I came across a really great post by Josh Pollock, which you can read here.
Josh tells us that we can use the rest_api_init
action and the rest_pre_serve_request
filter to adjust headers sent over the REST API. Therefore I had this code written up:
add_action( 'rest_api_init', function () { add_action( 'rest_pre_serve_request', function () { header( 'Access-Control-Allow-Headers: X-WP-Wpml-Language' ); } ); }, 15 );
This code would allow our new X-WP-Wpml-Language
header through, and it did. It worked fine in every browser, but then IE11 came along.
IE11 and HTTP Headers
So, lets dig into IE11 and see what’s the issue.
The REST API already sets some Access-Control-Allow-Headers
headers in it’s core code. We’ve then sent another header through with more Access-Control-Allow-Headers
headers. So the browser sees something like this:
Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Allow-Headers: X-WP-Wpml-Language
Sending headers like this should be fine and indeed all other browsers handle it correctly. but apparently IE11 can’t read headers split over multiple lines and therefore was only ever reading the first set (See here, here and here).
Don’t be fooled though, if you look in the IE11 developer tools, it will show you all the headers you sent on the same line as below, this makes it really confusing to debug.
Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Wpml-Language
So, to fix the CORS Preflight in IE11, we need to send all the headers in a single header request.
The Fix
Luckily, PHP is pretty cool in allowing us to override the headers that have already been set. Knowing we can do this, means we just needed to know all of the Access-Control-Allow-Headers
values that we needed. In this case, it’s Authorization
, Content-Type
and X-WP-Wpml-Language
.
We can adjust the code we used to allow the X-WP-Wpml-Language
header initially to allow all of our headers, like this:
add_action( 'rest_api_init', function () { add_action( 'rest_pre_serve_request', function () { header( 'Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Wpml-Language', true ); } ); }, 15 );
That second parameter of true
in the header()
function is the real hero here. It’s the replace
parameter and allows us to replace any headers of the same name that have been set before.
Doing this allows us to send a single header with all of our allowed values in it so that IE11 can read them all.
Conclusion
This was probably the error that took me the longest to debug when building this system. The biggest issue for me was the lack of info around that IE11 won’t handle multiple headers of the same name, especially when it’s developer tools show them all in one line.
I’m hoping that by documenting this here, I’ll remember it and it may help somebody in the future with this issue.