Thanks for the input Nigel! The issue turned out to be caused by an HTTP interceptor from an imported library which would set the Content-Type to application/json if it was undefined.

Finally fixed the problem by adding a new HTTP interceptor to remove the Content-Type header if the request URL matched a specific pattern (i.e. contained "$upload"). Removing the Content-Type header allowed the Content-Type to get auto-detected as "multipart/form-data" and the boundary to get set when the request was sent.