Using Microsoft Edge in a native Windows desktop app – part 4

This article in requiring Microsoft Edge WebView2 Runtime 88.0.705.50 or newer.

In the previous articles, we learned how to perform navigation in a Windows desktop application and how navigation events work. However, until recently, it was not possible to perform POST or GET request using custom headers or content. This feature was added in version 705.50. In this fourth article of the series, we will look in detail at how to perform POST requests with custom headers and content.

Overview

There are times when you must perform navigation to a page using a GET or POST request that requires custom headers or content (for a POST). This is not possible with the ICoreWebView2::Navigate() but it is available with ICoreWebView2_2::NavigateWithWebResourceRequest(). This method takes a pointer to an object implementing the ICoreWebView2WebResourceRequest interface. This interface defines an HTTP request, providing properties for the URI, method, headers, and content.

The argument passed to this function must be created with the ICoreWebView2Environment2::CreateWebResourceRequest() method. This method takes four input parameters for URI, method, content (i.e. post data), and headers, and an output parameter representing a pointer to the object that implements ICoreWebView2WebResourceRequest.

The headers specified when calling this function override the headers added by WebView2 runtime except for Cookie headers. The HTTP method can only be GET or POST. The content you specify is sent only if the method is POST and the scheme is HTTP or HTTPS.

Extending the CWebBrowser class

In this section, we will extent the CWebBrowser class seen in the previous articles, to support navigation with a POST request. For this purpose, we will first add a new method called NavigatePost():

class CWebBrowser : public CWnd
{
public:
   void NavigatePost(CString const& url, CString const& content, CString const& headers, CallbackFunc onComplete = nullptr);
};

In the previous section, I have mentioned two new interfaces added to the SDK to support this new feature: ICoreWebView2Environment2 and ICoreWebView2_2. We need to add pointers to these interfaces in order to call the required methods.

struct CWebBrowserImpl
{
   wil::com_ptr<ICoreWebView2Environment>    m_webViewEnvironment;
   wil::com_ptr<ICoreWebView2Environment2>   m_webViewEnvironment2;
   wil::com_ptr<ICoreWebView2>               m_webView;
   wil::com_ptr<ICoreWebView2_2>             m_webView2;
   wil::com_ptr<ICoreWebView2Controller>     m_webController;
   wil::com_ptr<ICoreWebView2Settings>       m_webSettings;
};

We need to make small changes to OnCreateEnvironmentCompleted() and OnCreateWebViewControllerCompleted() in order to initialize these variables.

HRESULT CWebBrowser::OnCreateEnvironmentCompleted(
   HRESULT result, 
   ICoreWebView2Environment* environment)
{
   CHECK_FAILURE(result);

   if (!environment)
      return E_FAIL;

   CHECK_FAILURE(environment->QueryInterface(IID_PPV_ARGS(&m_pImpl->m_webViewEnvironment)));
   CHECK_FAILURE(environment->QueryInterface(IID_PPV_ARGS(&m_pImpl->m_webViewEnvironment2)));

   if (!m_pImpl->m_webViewEnvironment)
      return E_FAIL;

   CHECK_FAILURE(m_pImpl->m_webViewEnvironment->CreateCoreWebView2Controller(
      m_hWnd,
      Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
         this,
         &CWebBrowser::OnCreateWebViewControllerCompleted).Get()));

   return S_OK;
}

HRESULT CWebBrowser::OnCreateWebViewControllerCompleted(
   HRESULT result,
   ICoreWebView2Controller* controller)
{
   if (result == S_OK)
   {
      if (controller != nullptr)
      {
         m_pImpl->m_webController = controller;
         CHECK_FAILURE(controller->get_CoreWebView2(&m_pImpl->m_webView));

         if (!m_pImpl->m_webView)
            return E_FAIL;

         CHECK_FAILURE(m_pImpl->m_webView->QueryInterface(IID_PPV_ARGS(&m_pImpl->m_webView2)));

         CHECK_FAILURE(m_pImpl->m_webView->get_Settings(&m_pImpl->m_webSettings));

         // We have a few of our own event handlers to register here as well
         RegisterEventHandlers();

         // Set the initial size of the WebView
         ResizeEverything();
      }

      auto callback = m_callbacks[CallbackType::CreationCompleted];
      if (callback != nullptr)
         RunAsync(callback);
   }
   else
   {
      CString text;
      GetAppObject()->GetLangText(TEXT_MSG, ERR_CANNOT_CREATE_WEBVIEW_ENV, 0, text);
      ShowFailure(result, text);
   }

   return S_OK;
}

These variables should be set to nullptr when closing the web view.

void CWebBrowser::CloseWebView()
{
   if (m_pImpl->m_webView)
   {
      m_pImpl->m_webView->remove_NavigationCompleted(m_navigationCompletedToken);
      m_pImpl->m_webView->remove_NavigationStarting(m_navigationStartingToken);
      m_pImpl->m_webView->remove_DocumentTitleChanged(m_documentTitleChangedToken);

      m_pImpl->m_webController->Close();

      m_pImpl->m_webController = nullptr;
      m_pImpl->m_webView = nullptr;
      m_pImpl->m_webView2 = nullptr;
      m_pImpl->m_webSettings = nullptr;
   }

   m_pImpl->m_webViewEnvironment = nullptr;
   m_pImpl->m_webViewEnvironment2 = nullptr;
}

The implementation of the NavigatePost() is fairly straight forward (based on the information from the Overview section):

// The raw request header string delimited by CRLF(optional in last header).
void CWebBrowser::NavigatePost(CString const& url, CString const& content, CString const& headers, std::function<void()> onComplete)
{
   if (!m_pImpl->m_webView) return;

   CString normalizedUrl{ NormalizeUrl(url) };

   m_callbacks[CallbackType::NavigationCompleted] = onComplete;
      
   wil::com_ptr<ICoreWebView2WebResourceRequest> webResourceRequest;
   wil::com_ptr<IStream> postDataStream = SHCreateMemStream(
      reinterpret_cast<const BYTE*>(static_cast<LPCTSTR>(content)),
      content.GetLength() + 1);

   CHECK_FAILURE(m_pImpl->m_webViewEnvironment2->CreateWebResourceRequest(
      CT2W(normalizedUrl),
      L"POST",
      postDataStream.get(),
      CT2W(headers),
      &webResourceRequest));

   CHECK_FAILURE(m_pImpl->m_webView2->NavigateWithWebResourceRequest(webResourceRequest.get()));
}

Putting it to test

To test this implementation, I have created a simple endpoint for a POST request using the Post Test Server V2 service. The endpoint description is available at https://ptsv2.com/t/jep76-1611756376. What we’re doing here, is making a POST request using basic authorization, and therefore requiring the Authorization header. There is no content that is passed, and the response has the following body:

<h1>Thank you for trying this demo.</h1>
<p>I hope you have a lovely day!</p>

We can navigate to this URL with the following code (notice that the base64 encoding of the username:password text is hardcoded for simplicity):

void CMainFrame::OnBnClickedButtonTestPost()
{
   auto view = dynamic_cast<CMfcEdgeDemoView*>(GetActiveView());
   if (view != nullptr)
   {
      CString content;

      // see https://ptsv2.com/t/jep76-1611756376
      CString headers = L"Authorization:Basic ZGVtbzpkZW1v\r\nUser-Agent:WebView2 Demo";

      view->NavigatePost(L"https://ptsv2.com/t/jep76-1611756376/post", content, headers);
   }
}

And this is the result of making this call:

Also, if we check the request dump at ptsv2.com, we can look at the headers. We can see the Authorization and the User-Agent headers had the content we provided in the previous snippet.

Try the app

You can download, build, and try the sample app for this series from here: MfcEdgeDemo.zip (20633 downloads ) .

5 Replies to “Using Microsoft Edge in a native Windows desktop app – part 4”

  1. Hi Marius,

    Thanks for the tutorial. Was very helpful for me.
    I tried to set the accept header with application/json and I observed that in the request dump at ptsv2.com, the Accept header doesn’t change.
    Do you know what can be the problem or need to proceed in another way when we want to change the accept header?

    Thanks in advance!

  2. Hi Marius Bancila,

    I would like to thank you for this article series. I would like to use EdgeWebBrowser class as a base in commercial application.

    In part 3 of this series you mentioned that the content is free to use with attribution under https://creativecommons.org/licenses/by/4.0/

    I am asking if this is still the case also for updated code in part 4.

    Thank you for clarification.

  3. It’s a really great sample.
    Edge browser was a must need for my current developing program. Thank you very much.
    Now, I want to open a specific URL(http://~~/login.php) and add an automatic login function using the login information(user ID/PW) entered in advance.
    Is it possible to implement such a thing?
    If possible, please advise on what I should do.

  4. It’s a really great sample.
    Edge browser was a must need for my current developing program. Thank you very much.
    Now, I want to open a specific URL (http://~~/login.php) and add an automatic login function using the login information (ID/PW) entered in advance. Is it possible to implement such a thing?
    If possible, please advise on what I should do.

  5. Hi Marius,
    I have implemented NavigateWithWebResourceRequest in one of my Win32 C++ application. There I have created webResourceRequest with post data and the post data content is something like below:
    ……………………..

    But while I am running this, it is showing “Error Processing Request”. It would be really helpful if you please help me out on this.

    Thank you !

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.