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.

Articles in this series:

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 (755 downloads) .

Leave a Reply

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