Browse Source

Merge pull request #7540 from deepak1556/form_post_new_window_patch

webContents: handle POST navigation for new windows
Kevin Sawicki 8 years ago
parent
commit
4867475cee

+ 10 - 7
atom/browser/api/atom_api_app.cc

@@ -528,12 +528,14 @@ void App::OnLogin(LoginHandler* login_handler,
     login_handler->CancelAuth();
 }
 
-void App::OnCreateWindow(const GURL& target_url,
-                         const std::string& frame_name,
-                         WindowOpenDisposition disposition,
-                         const std::vector<base::string16>& features,
-                         int render_process_id,
-                         int render_frame_id) {
+void App::OnCreateWindow(
+    const GURL& target_url,
+    const std::string& frame_name,
+    WindowOpenDisposition disposition,
+    const std::vector<base::string16>& features,
+    const scoped_refptr<content::ResourceRequestBodyImpl>& body,
+    int render_process_id,
+    int render_frame_id) {
   v8::Locker locker(isolate());
   v8::HandleScope handle_scope(isolate());
   content::RenderFrameHost* rfh =
@@ -545,7 +547,8 @@ void App::OnCreateWindow(const GURL& target_url,
     api_web_contents->OnCreateWindow(target_url,
                                      frame_name,
                                      disposition,
-                                     features);
+                                     features,
+                                     body);
   }
 }
 

+ 8 - 6
atom/browser/api/atom_api_app.h

@@ -48,12 +48,14 @@ class App : public AtomBrowserClient::Delegate,
                              v8::Local<v8::FunctionTemplate> prototype);
 
   // Called when window with disposition needs to be created.
-  void OnCreateWindow(const GURL& target_url,
-                      const std::string& frame_name,
-                      WindowOpenDisposition disposition,
-                      const std::vector<base::string16>& features,
-                      int render_process_id,
-                      int render_frame_id);
+  void OnCreateWindow(
+      const GURL& target_url,
+      const std::string& frame_name,
+      WindowOpenDisposition disposition,
+      const std::vector<base::string16>& features,
+      const scoped_refptr<content::ResourceRequestBodyImpl>& body,
+      int render_process_id,
+      int render_frame_id);
 
 #if defined(USE_NSS_CERTS)
   void OnCertificateManagerModelCreated(

+ 13 - 5
atom/browser/api/atom_api_web_contents.cc

@@ -387,12 +387,14 @@ bool WebContents::AddMessageToConsole(content::WebContents* source,
   }
 }
 
-void WebContents::OnCreateWindow(const GURL& target_url,
-                                 const std::string& frame_name,
-                                 WindowOpenDisposition disposition,
-                                 const std::vector<base::string16>& features) {
+void WebContents::OnCreateWindow(
+    const GURL& target_url,
+    const std::string& frame_name,
+    WindowOpenDisposition disposition,
+    const std::vector<base::string16>& features,
+    const scoped_refptr<content::ResourceRequestBodyImpl>& body) {
   if (type_ == BROWSER_WINDOW || type_ == OFF_SCREEN)
-    Emit("-new-window", target_url, frame_name, disposition, features);
+    Emit("-new-window", target_url, frame_name, disposition, features, body);
   else
     Emit("new-window", target_url, frame_name, disposition, features);
 }
@@ -855,6 +857,12 @@ void WebContents::LoadURL(const GURL& url, const mate::Dictionary& options) {
   if (options.Get("extraHeaders", &extra_headers))
     params.extra_headers = extra_headers;
 
+  scoped_refptr<content::ResourceRequestBodyImpl> body;
+  if (options.Get("postData", &body)) {
+    params.post_data = body;
+    params.load_type = content::NavigationController::LOAD_TYPE_HTTP_POST;
+  }
+
   params.transition_type = ui::PAGE_TRANSITION_TYPED;
   params.should_clear_history_list = true;
   params.override_user_agent = content::NavigationController::UA_OVERRIDE_TRUE;

+ 10 - 4
atom/browser/api/atom_api_web_contents.h

@@ -26,6 +26,10 @@ namespace brightray {
 class InspectableWebContents;
 }
 
+namespace content {
+class ResourceRequestBodyImpl;
+}
+
 namespace mate {
 class Arguments;
 class Dictionary;
@@ -176,10 +180,12 @@ class WebContents : public mate::TrackableObject<WebContents>,
                                    bool allowed);
 
   // Create window with the given disposition.
-  void OnCreateWindow(const GURL& target_url,
-                      const std::string& frame_name,
-                      WindowOpenDisposition disposition,
-                      const std::vector<base::string16>& features);
+  void OnCreateWindow(
+      const GURL& target_url,
+      const std::string& frame_name,
+      WindowOpenDisposition disposition,
+      const std::vector<base::string16>& features,
+      const scoped_refptr<content::ResourceRequestBodyImpl>& body);
 
   // Returns the web preferences of current WebContents.
   v8::Local<v8::Value> GetWebPreferences(v8::Isolate* isolate);

+ 3 - 1
atom/browser/atom_browser_client.cc

@@ -30,6 +30,7 @@
 #include "chrome/browser/renderer_host/pepper/chrome_browser_pepper_host_factory.h"
 #include "chrome/browser/renderer_host/pepper/widevine_cdm_message_filter.h"
 #include "chrome/browser/speech/tts_message_filter.h"
+#include "content/common/resource_request_body_impl.h"
 #include "content/public/browser/browser_ppapi_host.h"
 #include "content/public/browser/client_certificate_delegate.h"
 #include "content/public/browser/geolocation_delegate.h"
@@ -319,7 +320,7 @@ bool AtomBrowserClient::CanCreateWindow(
     WindowOpenDisposition disposition,
     const blink::WebWindowFeatures& features,
     const std::vector<base::string16>& additional_features,
-    const scoped_refptr<ResourceRequestBodyImpl>& body,
+    const scoped_refptr<content::ResourceRequestBodyImpl>& body,
     bool user_gesture,
     bool opener_suppressed,
     content::ResourceContext* context,
@@ -342,6 +343,7 @@ bool AtomBrowserClient::CanCreateWindow(
                                     frame_name,
                                     disposition,
                                     additional_features,
+                                    body,
                                     render_process_id,
                                     opener_render_frame_id));
   }

+ 19 - 20
atom/browser/atom_browser_client.h

@@ -13,8 +13,6 @@
 #include "brightray/browser/browser_client.h"
 #include "content/public/browser/render_process_host_observer.h"
 
-using content::ResourceRequestBodyImpl;
-
 namespace content {
 class QuotaPermissionContext;
 class ClientCertificateDelegate;
@@ -81,24 +79,25 @@ class AtomBrowserClient : public brightray::BrowserClient,
       net::SSLCertRequestInfo* cert_request_info,
       std::unique_ptr<content::ClientCertificateDelegate> delegate) override;
   void ResourceDispatcherHostCreated() override;
-  bool CanCreateWindow(const GURL& opener_url,
-                       const GURL& opener_top_level_frame_url,
-                       const GURL& source_origin,
-                       WindowContainerType container_type,
-                       const std::string& frame_name,
-                       const GURL& target_url,
-                       const content::Referrer& referrer,
-                       WindowOpenDisposition disposition,
-                       const blink::WebWindowFeatures& features,
-                       const std::vector<base::string16>& additional_features,
-                       const scoped_refptr<ResourceRequestBodyImpl>& body,
-                       bool user_gesture,
-                       bool opener_suppressed,
-                       content::ResourceContext* context,
-                       int render_process_id,
-                       int opener_render_view_id,
-                       int opener_render_frame_id,
-                       bool* no_javascript_access) override;
+  bool CanCreateWindow(
+      const GURL& opener_url,
+      const GURL& opener_top_level_frame_url,
+      const GURL& source_origin,
+      WindowContainerType container_type,
+      const std::string& frame_name,
+      const GURL& target_url,
+      const content::Referrer& referrer,
+      WindowOpenDisposition disposition,
+      const blink::WebWindowFeatures& features,
+      const std::vector<base::string16>& additional_features,
+      const scoped_refptr<content::ResourceRequestBodyImpl>& body,
+      bool user_gesture,
+      bool opener_suppressed,
+      content::ResourceContext* context,
+      int render_process_id,
+      int opener_render_view_id,
+      int opener_render_frame_id,
+      bool* no_javascript_access) override;
   void GetAdditionalAllowedSchemesForFileSystem(
       std::vector<std::string>* schemes) override;
 

+ 100 - 0
atom/common/native_mate_converters/content_converter.cc

@@ -14,10 +14,14 @@
 #include "atom/common/native_mate_converters/gurl_converter.h"
 #include "atom/common/native_mate_converters/string16_converter.h"
 #include "atom/common/native_mate_converters/ui_base_types_converter.h"
+#include "atom/common/native_mate_converters/value_converter.h"
+#include "content/common/resource_request_body_impl.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/context_menu_params.h"
 #include "native_mate/dictionary.h"
 
+using content::ResourceRequestBodyImpl;
+
 namespace {
 
 void ExecuteCommand(content::WebContents* web_contents,
@@ -195,6 +199,102 @@ bool Converter<content::StopFindAction>::FromV8(
   return true;
 }
 
+// static
+v8::Local<v8::Value>
+Converter<scoped_refptr<ResourceRequestBodyImpl>>::ToV8(
+    v8::Isolate* isolate,
+    const scoped_refptr<ResourceRequestBodyImpl>& val) {
+  if (!val)
+    return v8::Null(isolate);
+  std::unique_ptr<base::ListValue> list(new base::ListValue);
+  for (const auto& element : *(val->elements())) {
+    std::unique_ptr<base::DictionaryValue> post_data_dict(
+        new base::DictionaryValue);
+    auto type = element.type();
+    if (type == ResourceRequestBodyImpl::Element::TYPE_BYTES) {
+      std::unique_ptr<base::Value> bytes(
+          base::BinaryValue::CreateWithCopiedBuffer(
+              element.bytes(), static_cast<size_t>(element.length())));
+      post_data_dict->SetString("type", "rawData");
+      post_data_dict->Set("bytes", std::move(bytes));
+    } else if (type == ResourceRequestBodyImpl::Element::TYPE_FILE) {
+      post_data_dict->SetString("type", "file");
+      post_data_dict->SetStringWithoutPathExpansion(
+          "filePath", element.path().AsUTF8Unsafe());
+      post_data_dict->SetInteger("offset", static_cast<int>(element.offset()));
+      post_data_dict->SetInteger("length", static_cast<int>(element.length()));
+      post_data_dict->SetDouble(
+          "modificationTime", element.expected_modification_time().ToDoubleT());
+    } else if (type == ResourceRequestBodyImpl::Element::TYPE_FILE_FILESYSTEM) {
+      post_data_dict->SetString("type", "fileSystem");
+      post_data_dict->SetStringWithoutPathExpansion(
+          "fileSystemURL", element.filesystem_url().spec());
+      post_data_dict->SetInteger("offset", static_cast<int>(element.offset()));
+      post_data_dict->SetInteger("length", static_cast<int>(element.length()));
+      post_data_dict->SetDouble(
+          "modificationTime", element.expected_modification_time().ToDoubleT());
+    } else if (type == ResourceRequestBodyImpl::Element::TYPE_BLOB) {
+      post_data_dict->SetString("type", "blob");
+      post_data_dict->SetString("blobUUID", element.blob_uuid());
+    }
+    list->Append(std::move(post_data_dict));
+  }
+  return ConvertToV8(isolate, *list);
+}
+
+// static
+bool Converter<scoped_refptr<ResourceRequestBodyImpl>>::FromV8(
+    v8::Isolate* isolate,
+    v8::Local<v8::Value> val,
+    scoped_refptr<ResourceRequestBodyImpl>* out) {
+  std::unique_ptr<base::ListValue> list(new base::ListValue);
+  if (!ConvertFromV8(isolate, val, list.get()))
+    return false;
+  *out = new content::ResourceRequestBodyImpl();
+  for (size_t i = 0; i < list->GetSize(); ++i) {
+    base::DictionaryValue* dict = nullptr;
+    std::string type;
+    if (!list->GetDictionary(i, &dict))
+      return false;
+    dict->GetString("type", &type);
+    if (type == "rawData") {
+      base::BinaryValue* bytes = nullptr;
+      dict->GetBinary("bytes", &bytes);
+      (*out)->AppendBytes(bytes->GetBuffer(), bytes->GetSize());
+    } else if (type == "file") {
+      std::string file;
+      int offset = 0, length = -1;
+      double modification_time = 0.0;
+      dict->GetStringWithoutPathExpansion("filePath", &file);
+      dict->GetInteger("offset", &offset);
+      dict->GetInteger("file", &length);
+      dict->GetDouble("modificationTime", &modification_time);
+      (*out)->AppendFileRange(base::FilePath::FromUTF8Unsafe(file),
+                              static_cast<uint64_t>(offset),
+                              static_cast<uint64_t>(length),
+                              base::Time::FromDoubleT(modification_time));
+    } else if (type == "fileSystem") {
+      std::string file_system_url;
+      int offset = 0, length = -1;
+      double modification_time = 0.0;
+      dict->GetStringWithoutPathExpansion("fileSystemURL", &file_system_url);
+      dict->GetInteger("offset", &offset);
+      dict->GetInteger("file", &length);
+      dict->GetDouble("modificationTime", &modification_time);
+      (*out)->AppendFileSystemFileRange(
+          GURL(file_system_url),
+          static_cast<uint64_t>(offset),
+          static_cast<uint64_t>(length),
+          base::Time::FromDoubleT(modification_time));
+    } else if (type == "blob") {
+      std::string uuid;
+      dict->GetString("blobUUID", &uuid);
+      (*out)->AppendBlob(uuid);
+    }
+  }
+  return true;
+}
+
 // static
 v8::Local<v8::Value> Converter<content::WebContents*>::ToV8(
     v8::Isolate* isolate, content::WebContents* val) {

+ 10 - 0
atom/common/native_mate_converters/content_converter.h

@@ -15,6 +15,7 @@
 
 namespace content {
 struct ContextMenuParams;
+class ResourceRequestBodyImpl;
 class WebContents;
 }
 
@@ -47,6 +48,15 @@ struct Converter<content::PermissionType> {
                                    const content::PermissionType& val);
 };
 
+template<>
+struct Converter<scoped_refptr<content::ResourceRequestBodyImpl>> {
+  static v8::Local<v8::Value> ToV8(
+      v8::Isolate* isolate,
+      const scoped_refptr<content::ResourceRequestBodyImpl>& val);
+  static bool FromV8(v8::Isolate* isolate, v8::Local<v8::Value> val,
+                     scoped_refptr<content::ResourceRequestBodyImpl>* out);
+};
+
 template<>
 struct Converter<content::StopFindAction> {
   static bool FromV8(v8::Isolate* isolate, v8::Local<v8::Value> val,

+ 1 - 0
docs/api/browser-window.md

@@ -969,6 +969,7 @@ Same as `webContents.capturePage([rect, ]callback)`.
   * `httpReferrer` String (optional) - A HTTP Referrer url.
   * `userAgent` String (optional) - A user agent originating the request.
   * `extraHeaders` String (optional) - Extra headers separated by "\n"
+  * `postData` ([UploadRawData](structures/upload-raw-data.md) | [UploadFile](structures/upload-file.md) | [UploadFileSystem](structures/upload-file-system.md) | [UploadBlob](structures/upload-blob.md))[] (optional)
 
 Same as `webContents.loadURL(url[, options])`.
 

+ 4 - 0
docs/api/structures/upload-blob.md

@@ -0,0 +1,4 @@
+# Upload Blob Object
+
+* `type` String - `blob`.
+* `blobUUID` String - UUID of blob data to upload.

+ 9 - 0
docs/api/structures/upload-file-system.md

@@ -0,0 +1,9 @@
+# Upload FileSystem Object
+
+* `type` String - `fileSystem`.
+* `filsSystemURL` String - FileSystem url to read data for upload.
+* `offset` Integer - Defaults to `0`.
+* `length` Integer - Number of bytes to read from `offset`.
+  Defaults to `0`.
+* `modificationTime` Double - Last Modification time in
+  number of seconds sine the UNIX epoch.

+ 9 - 0
docs/api/structures/upload-file.md

@@ -0,0 +1,9 @@
+# Upload File Object
+
+* `type` String - `file`.
+* `filePath` String - Path of file to be uploaded.
+* `offset` Integer - Defaults to `0`.
+* `length` Integer - Number of bytes to read from `offset`.
+  Defaults to `0`.
+* `modificationTime` Double - Last Modification time in
+  number of seconds sine the UNIX epoch.

+ 4 - 0
docs/api/structures/upload-raw-data.md

@@ -0,0 +1,4 @@
+# Upload RawData Object
+
+* `type` String - `rawData`.
+* `bytes` Buffer - Data to be uploaded.

+ 4 - 3
docs/api/web-contents.md

@@ -487,9 +487,10 @@ win.loadURL('http://github.com')
 
 * `url` URL
 * `options` Object (optional)
-  * `httpReferrer` String - A HTTP Referrer url.
-  * `userAgent` String - A user agent originating the request.
-  * `extraHeaders` String - Extra headers separated by "\n"
+  * `httpReferrer` String (optional) - A HTTP Referrer url.
+  * `userAgent` String (optional) - A user agent originating the request.
+  * `extraHeaders` String (optional) - Extra headers separated by "\n"
+  * `postData` ([UploadRawData](structures/upload-raw-data.md) | [UploadFile](structures/upload-file.md) | [UploadFileSystem](structures/upload-file-system.md) | [UploadBlob](structures/upload-blob.md))[] (optional)
 
 Loads the `url` in the window. The `url` must contain the protocol prefix,
 e.g. the `http://` or `file://`. If the load should bypass http cache then

+ 4 - 3
docs/api/web-view-tag.md

@@ -262,9 +262,10 @@ webview.addEventListener('dom-ready', () => {
 
 * `url` URL
 * `options` Object (optional)
-  * `httpReferrer` String - A HTTP Referrer url.
-  * `userAgent` String - A user agent originating the request.
-  * `extraHeaders` String - Extra headers separated by "\n"
+  * `httpReferrer` String (optional) - A HTTP Referrer url.
+  * `userAgent` String (optional) - A user agent originating the request.
+  * `extraHeaders` String (optional) - Extra headers separated by "\n"
+  * `postData` ([UploadRawData](structures/upload-raw-data.md) | [UploadFile](structures/upload-file.md) | [UploadFileSystem](structures/upload-file-system.md) | [UploadBlob](structures/upload-blob.md))[] (optional)
 
 Loads the `url` in the webview, the `url` must contain the protocol prefix,
 e.g. the `http://` or `file://`.

+ 6 - 2
lib/browser/api/browser-window.js

@@ -18,13 +18,17 @@ BrowserWindow.prototype._init = function () {
   }
 
   // Make new windows requested by links behave like "window.open"
-  this.webContents.on('-new-window', (event, url, frameName, disposition, additionalFeatures) => {
+  this.webContents.on('-new-window', (event, url, frameName,
+                                      disposition, additionalFeatures,
+                                      postData) => {
     const options = {
       show: true,
       width: 800,
       height: 600
     }
-    ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', event, url, frameName, disposition, options, additionalFeatures)
+    ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN',
+                 event, url, frameName, disposition,
+                 options, additionalFeatures, postData)
   })
 
   this.webContents.on('-web-contents-created', (event, webContents, url,

+ 18 - 4
lib/browser/guest-window-manager.js

@@ -84,7 +84,7 @@ const setupGuest = function (embedder, frameName, guest, options) {
 }
 
 // Create a new guest created by |embedder| with |options|.
-const createGuest = function (embedder, url, frameName, options) {
+const createGuest = function (embedder, url, frameName, options, postData) {
   let guest = frameToGuest[frameName]
   if (frameName && (guest != null)) {
     guest.loadURL(url)
@@ -119,7 +119,19 @@ const createGuest = function (embedder, url, frameName, options) {
     //
     // The above code would not work if a navigation to "about:blank" is done
     // here, since the window would be cleared of all changes in the next tick.
-    guest.loadURL(url)
+    const loadOptions = {}
+    if (postData != null) {
+      loadOptions.postData = postData
+      loadOptions.extraHeaders = 'content-type: application/x-www-form-urlencoded'
+      if (postData.length > 0) {
+        const postDataFront = postData[0].bytes.toString()
+        const boundary = /^--.*[^-\r\n]/.exec(postDataFront)
+        if (boundary != null) {
+          loadOptions.extraHeaders = `content-type: multipart/form-data; boundary=${boundary[0].substr(2)}`
+        }
+      }
+    }
+    guest.loadURL(url, loadOptions)
   }
 
   return setupGuest(embedder, frameName, guest, options)
@@ -140,7 +152,9 @@ const getGuestWindow = function (guestId) {
 }
 
 // Routed window.open messages.
-ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function (event, url, frameName, disposition, options, additionalFeatures) {
+ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function (event, url, frameName,
+                                                                  disposition, options,
+                                                                  additionalFeatures, postData) {
   options = mergeBrowserWindowOptions(event.sender, options)
   event.sender.emit('new-window', event, url, frameName, disposition, options, additionalFeatures)
   const newGuest = event.newGuest
@@ -151,7 +165,7 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function (event, url, fr
       event.returnValue = null
     }
   } else {
-    event.returnValue = createGuest(event.sender, url, frameName, options)
+    event.returnValue = createGuest(event.sender, url, frameName, options, postData)
   }
 })
 

+ 89 - 10
spec/api-browser-window-spec.js

@@ -4,28 +4,59 @@ const assert = require('assert')
 const fs = require('fs')
 const path = require('path')
 const os = require('os')
+const qs = require('querystring')
 const http = require('http')
 const {closeWindow} = require('./window-helpers')
 
-const remote = require('electron').remote
-const screen = require('electron').screen
-
-const app = remote.require('electron').app
-const ipcMain = remote.require('electron').ipcMain
-const ipcRenderer = require('electron').ipcRenderer
-const BrowserWindow = remote.require('electron').BrowserWindow
+const {ipcRenderer, remote, screen} = require('electron')
+const {app, ipcMain, BrowserWindow, protocol} = remote
 
 const isCI = remote.getGlobal('isCi')
-const {protocol} = remote
 
 describe('browser-window module', function () {
   var fixtures = path.resolve(__dirname, 'fixtures')
   var w = null
-  var server
+  var server, postData
 
   before(function (done) {
+    const filePath = path.join(fixtures, 'pages', 'a.html')
+    const fileStats = fs.statSync(filePath)
+    postData = [
+      {
+        type: 'rawData',
+        bytes: new Buffer('username=test&file=')
+      },
+      {
+        type: 'file',
+        filePath: filePath,
+        offset: 0,
+        length: fileStats.size,
+        modificationTime: fileStats.mtime.getTime() / 1000
+      }
+    ]
     server = http.createServer(function (req, res) {
-      function respond () { res.end('') }
+      function respond () {
+        if (req.method === 'POST') {
+          let body = ''
+          req.on('data', (data) => {
+            if (data) {
+              body += data
+            }
+          })
+          req.on('end', () => {
+            let parsedData = qs.parse(body)
+            fs.readFile(filePath, (err, data) => {
+              if (err) return
+              if (parsedData.username === 'test' &&
+                  parsedData.file === data.toString()) {
+                res.end()
+              }
+            })
+          })
+        } else {
+          res.end()
+        }
+      }
       setTimeout(respond, req.url.includes('slow') ? 200 : 0)
     })
     server.listen(0, '127.0.0.1', function () {
@@ -187,6 +218,54 @@ describe('browser-window module', function () {
       })
       w.loadURL('http://127.0.0.1:11111')
     })
+
+    describe('POST navigations', function () {
+      afterEach(() => {
+        w.webContents.session.webRequest.onBeforeSendHeaders(null)
+      })
+
+      it('supports specifying POST data', function (done) {
+        w.webContents.on('did-finish-load', () => done())
+        w.loadURL(server.url, {postData: postData})
+      })
+
+      it('sets the content type header on URL encoded forms', function (done) {
+        w.webContents.on('did-finish-load', () => {
+          w.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
+            assert.equal(details.requestHeaders['content-type'], 'application/x-www-form-urlencoded')
+            done()
+          })
+          w.webContents.executeJavaScript(`
+            form = document.createElement('form')
+            form.method = 'POST'
+            form.target = '_blank'
+            form.submit()
+          `)
+        })
+        w.loadURL(server.url)
+      })
+
+      it('sets the content type header on multi part forms', function (done) {
+        w.webContents.on('did-finish-load', () => {
+          w.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
+            assert(details.requestHeaders['content-type'].startsWith('multipart/form-data; boundary=----WebKitFormBoundary'))
+            done()
+          })
+          w.webContents.executeJavaScript(`
+            form = document.createElement('form')
+            form.method = 'POST'
+            form.target = '_blank'
+            form.enctype = 'multipart/form-data'
+            file = document.createElement('input')
+            file.type = 'file'
+            file.name = 'file'
+            form.appendChild(file)
+            form.submit()
+          `)
+        })
+        w.loadURL(server.url)
+      })
+    })
   })
 
   describe('BrowserWindow.show()', function () {