14

So, I have a rather complex issue to detail out here. the TL;DR is that Link Manager is returning inconsistent results in the GetServerUrl() method when given what appears to be the same parameters.

To set this up, I am using Sitecore 8.2 Update 2 with EXM 3.4 Update 1. I have, for all practical purposes, a vanilla install and setup of both Sitecore and EXM, with the exception of an additional site entry, which is the site that we are working with, and a content Home node.

Symptom of Issue

Dispatching an Email (Scenario 1)

When I dispatch a very simple email from EXM, that contains one link, which is to the Home page of the site that is configured as the EXM Site, the link that is created looks like this:

http://brandname.local/sitecore/RedirectUrlPage.aspx?ec_contact_id=1814C7BC2B434DC096F74115AB45104A&ec_message_id=2E6A28769FBB45F2AF36F956FA3B8021&ec_url=%2f%3a%2f%2fbrandname.local%2f

The parameter ec_url is the parameter that I am paying attention to. When EXM resolves the Click action, it redirects the user to http://brandname.local/://brandname.local/. That link generates a protection error because you can't have colons in the url past the protocol portion.

Site Scheme

I am aware that the reason why ://brandname.local/ is created is because the site definition doesn't have the scheme attribute set to http. While that makes this a valid link for the purposes of the redirect, the proper link should be relative, not absolute (if I understand correctly).

Expected Result

Looking at the Email Message in EXM (Scenario 2)

When I look at the email message in EXM (Third Tab[Message] when looking at a dispatch), all of the same pipelines run that generate these links, except that now the link is generated correctly. I also found out that when looking at the email from here, PreviewMode is false, so it creates the same url as if dispatching. By correctly, I mean, because SiteResolving returns the brandname site, and EXM is configured to use the brandname site, the links generated look like this:

http://brandname.local/sitecore/RedirectUrlPage.aspx?ec_contact_id=1814C7BC2B434DC096F74115AB45104A&ec_message_id=2E6A28769FBB45F2AF36F956FA3B8021&ec_url=%2f

When that is clicked on, it resolves/redirects to http://brandname.local/ which is the expected behavior.

Enter LinkManager

EXM uses LinkManager to resolve these links.

In the HandleInternalLink processor for the <modifyHyperlink> pipeline, you find the following code:

            DynamicLink result;
            if (args.Url.IndexOf("~/link.aspx?", StringComparison.InvariantCulture) >= 0 &&
                DynamicLink.TryParse(args.Url, out result))
            {
                var defaultUrlOptions = LinkManager.GetDefaultUrlOptions();
                defaultUrlOptions.SiteResolving = true;
                defaultUrlOptions.Site = SiteContext.GetSite(args.WebsiteConfigurationName);
                args.Url = LinkManager.GetItemUrl(new ItemUtilExt().GetItem(result.ItemId), defaultUrlOptions);
            }

Now, becasuse my link in the Rich Text box is a dynamic link that points to a Home item, this processor uses LinkManager.GetItemUrl() to fetch the Url.

WebsiteConfigurationName

Note that args.WebsiteConfigurationName equals the site name that is configured on the EXM Manager Root. In both Scenario 1 and 2 (because the same Manager Root is being used), this is set to brandname. This maps back to Sitecore's site definition.

SiteResolving

I am also aware that SiteResolving works best when there is a targetHostName defined on the site definition.

LinkManager GetServerUrlElement()

Way down the line of LinkManager.GetItemUrl(), LinkManager executes the GetServerUrlElement() method, that does the heavy lifting of creating the server element portion of this.

  protected virtual string GetServerUrlElement(SiteInfo siteInfo)
  {
    SiteContext site = Context.Site;
    string str1 = site != null ? site.Name : string.Empty;
    string hostName = this.GetHostName();
    string str2 = this.AlwaysIncludeServerUrl ? WebUtil.GetServerUrl() : string.Empty;
    if (siteInfo == null)
      return str2;
    string str3;
    if (string.IsNullOrEmpty(siteInfo.HostName) || string.IsNullOrEmpty(hostName) || !siteInfo.Matches(hostName))
      str3 = StringUtil.GetString(new string[2]
      {
        this.GetTargetHostName(siteInfo),
        hostName
      });
    else
      str3 = hostName;
    string str4 = str3;
    string str5 = StringUtil.GetString(new string[2]{ siteInfo.Scheme, this.GetScheme() });
    int defaultValue = MainUtil.GetInt(siteInfo.Port, WebUtil.GetPort());
    int port = WebUtil.GetPort();
    int num = MainUtil.GetInt(siteInfo.ExternalPort, defaultValue);
    if (num != defaultValue)
    {
      if (this.AlwaysIncludeServerUrl)
        str2 = num == 80 ? string.Format("{0}://{1}", (object) str5, (object) this.GetHostName()) : string.Format("{0}://{1}:{2}", (object) str5, (object) this.GetHostName(), (object) num);
      defaultValue = num;
    }
    if (!this.AlwaysIncludeServerUrl && siteInfo.Name.Equals(str1, StringComparison.OrdinalIgnoreCase) && hostName.Equals(str4, StringComparison.OrdinalIgnoreCase) || (string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0))
      return str2;
    string scheme = this.GetScheme();
    StringComparison comparisonType = StringComparison.OrdinalIgnoreCase;
    if (str4.Equals(hostName, comparisonType) && defaultValue == port && str5.Equals(scheme, comparisonType))
      return str2;
    string str6 = str5 + "://" + str4;
    if (defaultValue > 0 && defaultValue != 80)
      str6 = str6 + ":" + (object) defaultValue;
    return str6;
  }

Debugging (using dotPeek Symbol Server) reveals the following portions of the above code are the exits points for each scenario.

Scenario 1 Returns "://brandname.local" At

    string str6 = str5 + "://" + str4;
    if (defaultValue > 0 && defaultValue != 80)
      str6 = str6 + ":" + (object) defaultValue;
    return str6;

Scenario 2 Returns ""(String.Empty) At

    if (str4.Equals(hostName, comparisonType) && defaultValue == port && str5.Equals(scheme, comparisonType))
      return str2;

Configuration

Site Config

<site name="brandname" virtualFolder="/" physicalFolder="/" rootPath="/sitecore/content/BrandName" startItem="/Home" database="web" domain="extranet" allowDebug="true" cacheHtml="true" htmlCacheSize="1000MB" renderingParametersCacheSize="200MB" xslCacheSize="100MB" filteredItemsCacheSize="200MB" enablePreview="true" enableWebEdit="true" enableDebugger="true" disableClientData="false" hostName="brandname.local" notFoundPage="/sitecore/content/BrandName/Home/404" itemRedirectPath="/sitecore/content/BrandName/Redirects/ItemBased" legacyRedirectPath="/sitecore/content/BrandName/Redirects/Legacy" patch:source="BrandName.SiteDefintion.config"/>

Site, Site Info, defaultUrlOptions

The following is the raw output of my debugging session, looking at the three pieces that are used in HandleInternalLink processor:

  • defaultUrlOptions.Site
  • defaultUrlOptions
  • defaultUrlOptions.Site.SiteInfo

I have indicated the pieces that are different according to BeyondCompare.

Scenario 1 - When the link doesn't generate right

defaultUrlOptions.Site
"brandname"
    AllowDebug: true
    BrowserTitle: "BrandName - Sitecore"
    CacheHtml: true   <--- Different
    CacheMedia: true   <--- Different
    Caches: {Sitecore.Sites.SiteCaches}
    ContentDatabase: null
    "": ""
    ContentStartItem: ""
    ContentStartPath: "/sitecore/content/BrandName"
    Database: {web}   <--- Different
    DefaultDevice: ""
    Device: ""
    DictionaryDomain: ""
    DisableBrowserCaching: true
    DisableClientData: false
    DisableFiltering: false
    DisableWebEditEditing: false
    DisableWebEditSelecting: false
    DisableXmlControls: false
    DisplayDate: {3/10/2017 2:40:34 AM}   <--- Different
    DisplayMode: Normal   <--- Different
    Domain: extranet   <--- Different
    DrawDots: false   <--- Different
    EnableDebugger: true
    EnableLinkedItems: true
    EnablePreview: true
    EnableWebEdit: true
    EnableWorkflow: false   <--- Different
    FilterItems: false   <--- Different
    HostName: "brandname.local"
    Language: "en"
    LoginPage: ""
    MasterDatabase: null
    MediaCachePath: "/App_Data/MediaCache/brandname"
    Name: "brandname"
    Notifications: {Sitecore.Events.NotificationContext}
    Page: {Sitecore.Layouts.PageContext}
    PhysicalFolder: "/"
    Properties: {System.Collections.Specialized.NameValueCollection}
    Request: {Sitecore.Sites.SiteRequest}
    RequireLogin: false
    Resources: {Sitecore.Resources.ResourceContext}
    Response: {Sitecore.Sites.SiteResponse}
    RootPath: "/sitecore/content/BrandName"
    SiteInfo: "brandname" ("web"#"")
    StartItem: "/Home"
    StartPath: "/sitecore/content/BrandName/Home"
    TargetHostName: "brandname.local"
    VirtualFolder: "/"
    XmlControlPage: "/layouts/xmlcontrol.aspx"
defaultUrlOptions
{Sitecore.Links.UrlOptions}
    AddAspxExtension: false
    AlwaysIncludeServerUrl: false
    EncodeNames: true
    "en": "en"
    LanguageEmbedding: Never
    LanguageLocation: FilePath
    LowercaseUrls: true
    ShortenUrls: true
    Site: "brandname"
    SiteResolving: true
    UseDisplayName: false
defaultUrlOptions.Site.SiteInfo
"brandname" ("web"#"")
    AllowDebug: true
    BrowserTitle: "BrandName - Sitecore"
    CacheHtml: true
    CacheMedia: true
    ContentLanguage: ""
    ContentStartItem: ""
    Database: "web"
    DefaultDevice: ""
    Device: ""
    DictionaryDomain: ""
    DisableBrowserCaching: true
    DisableClientData: false
    DisableXmlControls: false
    Domain: "extranet"
    EnableDebugger: true
    EnableFieldLanguageFallback: false
    EnableItemLanguageFallback: false
    EnableLinkedItems: false
    EnablePreview: true
    EnableWebEdit: true
    EnableWorkflow: false
    EnforceVersionPresence: false
    ExternalPort: 0
    FilterItems: false
    FilteredItemsCache: {Sitecore.Caching.FilteredItemsCache}
    HostName: "brandname.local"
    HtmlCache: {Sitecore.Caching.HtmlCache}
    HtmlCacheClearLatency: ""
    IsActive: true
    Language: ""
    LoginPage: ""
    MasterDatabase: ""
    MediaCachePath: ""
    Name: "brandname"
    PhysicalFolder: "/"
    Port: 0
    Properties: {System.Collections.Specialized.NameValueCollection}
    RegistryCache: {Sitecore.Caching.RegistryCache}
    RenderingParametersCache: null
    RequireLogin: false
    RootPath: "/sitecore/content/BrandName"
    Scheme: ""
    StartItem: "/Home"
    TargetHostName: ""
    ViewStateCache: {Sitecore.Caching.ViewStateCache}
    VirtualFolder: "/"
    XmlControlPage: ""
    XslCache: {Sitecore.Caching.XslCache}

Scenario 2 - When the link generates correctly

defaultUrlOptions.Site
"brandname"
    AllowDebug: true
    BrowserTitle: "BrandName - Sitecore"
    CacheHtml: false   <--- Different
    CacheMedia: false   <--- Different
    Caches: {Sitecore.Sites.SiteCaches}
    ContentDatabase: null
    "": ""
    ContentStartItem: ""
    ContentStartPath: "/sitecore/content/BrandName"
    Database: {master}   <--- Different
    DefaultDevice: ""
    Device: ""
    DictionaryDomain: ""
    DisableBrowserCaching: true
    DisableClientData: false
    DisableFiltering: false
    DisableWebEditEditing: false
    DisableWebEditSelecting: false
    DisableXmlControls: false
    DisplayDate: {3/10/2017 2:44:46 AM}   <--- Different
    DisplayMode: Edit   <--- Different
    Domain: sitecore   <--- Different
    DrawDots: true   <--- Different
    EnableDebugger: true
    EnableLinkedItems: true
    EnablePreview: true
    EnableWebEdit: true
    EnableWorkflow: true   <--- Different
    FilterItems: true   <--- Different
    HostName: "brandname.local"
    Language: "en"
    LoginPage: ""
    MasterDatabase: null
    MediaCachePath: "/App_Data/MediaCache/brandname"
    Name: "brandname"
    Notifications: {Sitecore.Events.NotificationContext}
    Page: {Sitecore.Layouts.PageContext}
    PhysicalFolder: "/"
    Properties: {System.Collections.Specialized.NameValueCollection}
    Request: {Sitecore.Sites.SiteRequest}
    RequireLogin: false
    Resources: {Sitecore.Resources.ResourceContext}
    Response: {Sitecore.Sites.SiteResponse}
    RootPath: "/sitecore/content/BrandName"
    SiteInfo: "brandname" ("web"#"")
    StartItem: "/Home"
    StartPath: "/sitecore/content/BrandName/Home"
    TargetHostName: "brandname.local"
    VirtualFolder: "/"
    XmlControlPage: "/layouts/xmlcontrol.aspx"
defaultUrlOptions
{Sitecore.Links.UrlOptions}
    AddAspxExtension: false
    AlwaysIncludeServerUrl: false
    EncodeNames: true
    "en": "en"
    LanguageEmbedding: Never
    LanguageLocation: FilePath
    LowercaseUrls: true
    ShortenUrls: true
    Site: "brandname"
    SiteResolving: true
    UseDisplayName: false
defaultUrlOptions.Site.SiteInfo
"brandname" ("web"#"")
    AllowDebug: true
    BrowserTitle: "BrandName - Sitecore"
    CacheHtml: true
    CacheMedia: true
    ContentLanguage: ""
    ContentStartItem: ""
    Database: "web"
    DefaultDevice: ""
    Device: ""
    DictionaryDomain: ""
    DisableBrowserCaching: true
    DisableClientData: false
    DisableXmlControls: false
    Domain: "extranet"
    EnableDebugger: true
    EnableFieldLanguageFallback: false
    EnableItemLanguageFallback: false
    EnableLinkedItems: false
    EnablePreview: true
    EnableWebEdit: true
    EnableWorkflow: false
    EnforceVersionPresence: false
    ExternalPort: 0
    FilterItems: false
    FilteredItemsCache: {Sitecore.Caching.FilteredItemsCache}
    HostName: "brandname.local"
    HtmlCache: {Sitecore.Caching.HtmlCache}
    HtmlCacheClearLatency: ""
    IsActive: true
    Language: ""
    LoginPage: ""
    MasterDatabase: ""
    MediaCachePath: ""
    Name: "brandname"
    PhysicalFolder: "/"
    Port: 0
    Properties: {System.Collections.Specialized.NameValueCollection}
    RegistryCache: {Sitecore.Caching.RegistryCache}
    RenderingParametersCache: null
    RequireLogin: false
    RootPath: "/sitecore/content/BrandName"
    Scheme: ""
    StartItem: "/Home"
    TargetHostName: ""
    ViewStateCache: {Sitecore.Caching.ViewStateCache}
    VirtualFolder: "/"
    XmlControlPage: ""
    XslCache: {Sitecore.Caching.XslCache}

Question (Finally)

What in the world am I missing that would cause LinkManager to operate so differently between these two scenarios? My understanding is that EXM, if the site configured on the Manager Root and the Site resolved by the item are the same site, ec_url should be a relative URL. But for some reason, in one scenario, I am getting an absolute URL.

3
  • Just to confirm, you have two site entries setup, do you have a Manager Root for each of those sites?
    – Ian Graham
    Commented Mar 14, 2017 at 16:11
  • Sites are brandname listed abovex and then default <website> listed after it. One EXM Manager Root, configured for the brandname site. Commented Mar 14, 2017 at 16:14
  • Seems like a hard one to pin down- I did a quick test with the same version of EXM vanilla 8.22 instance. Just copied the site definition from the default website, added a hostname, targethostname on each. Links were all generated correctly.
    – Ian Graham
    Commented Mar 14, 2017 at 20:44

3 Answers 3

7
+100

Let me start by saying that I believe that this is a bug in Sitecore, and the results of our peer debugging session have strengthened that belief.

Solution/Workaround

You should be able to work around this issue by adding an asterisk (*) to the hostName of the <site> definition node for your site, and leaving the targetHostName blank, as you currently have it. For example, you could do:

<site hostName="*brandname.local" ... targetHostName="" />

What this is going to do is force the LinkProvider.GetServerUrlElement(...) method to return a relative URL, as it will not have enough information to parse the actual server URL. The method is smart enough to recognize the asterisk in the hostName and knows that it cannot strip it from or use the property's value.

Analyzing the Workaround and the Bug

Note that this workround was found after an hour of mental variable substitution, which I did - at first - attempt to include here, but the post became far too long. I will leave it to you to do the variable substitution on your own, and will instead discuss specific parts of the substitution that are particularly critical to the solution and understanding the bugs.

For starters, let's have a look at the below snippet, noting that I have added "markers" in comments to help identify different pieces of logic in our discussion:

// Marker 1
if (!this.AlwaysIncludeServerUrl && siteInfo.Name.Equals(str1, StringComparison.OrdinalIgnoreCase) && hostName.Equals(str4, StringComparison.OrdinalIgnoreCase) || (string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0))
    return str2;
string scheme = this.GetScheme();
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase;
// Marker 2
if (str4.Equals(hostName, comparisonType) && defaultValue == port && str5.Equals(scheme, comparisonType))
    return str2;
// Marker 3
string str6 = str5 + "://" + str4;
// Marker 4
if (defaultValue > 0 && defaultValue != 80)
    str6 = str6 + ":" + (object) defaultValue;
return str6;

Reverse Engineering

The OP gives us a pretty good amount of information with which to debug this issue, including giving us the exit points for each scenario.

Scenario 2

Scenario 2 works correctly, and returns inside the conditional at Marker 2:

// Marker 2
if (str4.Equals(hostName, comparisonType) && defaultValue == port && str5.Equals(scheme, comparisonType))
  return str2;

The returned value is stored in the variable str2 which means, since we are looking for a relative URL - which Scenario 2 has - and a relative URL should have an empty string for a server URL, that str2 is equal to string.Empty. We can validate this by looking up in the method and seeing that the following line is used to set str2:

string str2 = this.AlwaysIncludeServerUrl ? WebUtil.GetServerUrl() : string.Empty

Since we know that AlwaysIncludeServerUrl is false and the value of str2 is never modified so long as AlwaysIncludeServerUrl is false, we know that with or without the workaround str2 holds the value we want to return.

Given the above and what we know from the OP, we can conclude that all of the following are true before getting to Marker 2:

// returned value
str2 == string.Empty

// required based on AND logic
str4 == hostName
defaultValue == port
str5 == scheme

Scenario 1

Let's now have a look over at Scenario 1, which returns after Marker 4. The first thing we will do is have a look at Marker 3, where the variable with th return value is initialized, and the conditional at Marker 4.

// Marker 3
string str6 = str5 + "://" + str4;
// Marker 4
if (defaultValue > 0 && defaultValue != 80)
    str6 = str6 + ":" + (object) defaultValue;
return str6;

Looking at the returned value in the OP, ://brandname.local, and the fact that we know that str6 holds the returned value, we can conclude that all of the following are true before Marker 3:

// nothing before the "://" in the returned value
str5 == string.Empty   

// value after the "://" in the URL             
str4 == "brandname.local" 

// there is no port number appended to the returned value
defaultValue <= 0 || defaultValue == 80

Looking at the Rest of the Method:

Just looking at what we have analyzed from the exit points of Scenario 1 and Scenario 2, we do not yet have enough information to find the workaround. Have a look at the conditional at Marker 1:

// Marker 1
if (!this.AlwaysIncludeServerUrl && siteInfo.Name.Equals(str1, StringComparison.OrdinalIgnoreCase) && hostName.Equals(str4, StringComparison.OrdinalIgnoreCase) || (string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0))
    return str2;

Note that for both scenarios, this condition evaluated to false. Thus, we know that the following is false:

!this.AlwaysIncludeServerUrl && 
    siteInfo.Name.Equals(str1, StringComparison.OrdinalIgnoreCase) && 
    hostName.Equals(str4, StringComparison.OrdinalIgnoreCase) || 
    (string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0)

Substituting in what we know from the OP and what we have learned so far, we get:

!(false) && 
    siteInfo.Name.Equals(str1, StringComparison.OrdinalIgnoreCase) && 
    hostName.Equals(str4, StringComparison.OrdinalIgnoreCase) ||
    (string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0)

If we look at the logic that actually sets str4, we can see that it is set to the return of the GetTargetHostName(...) method, unless that value is null or empty, in which case it falls back to the hostName variable, which is the result of the GetHostName() method, which encapsulates the WebUtil.GetHostName() method.

Have a look at the GetTargetHostName(...) method:

  protected virtual string GetTargetHostName(SiteInfo siteInfo)
  {
    Assert.ArgumentNotNull((object) siteInfo, "siteInfo");
    if (!siteInfo.TargetHostName.IsNullOrEmpty())
      return siteInfo.TargetHostName;
    string hostName = siteInfo.HostName;
    if (hostName.IndexOfAny(new char[2]{ '*', '|' }) < 0)
      return hostName;
    return string.Empty;
  }

Here, we can see that if siteInfo.TargetHostName value is an empty string, which we know to be the case from the OP, then the hostName is returned so long as it doesn't contain an asterisk or a pipe (|). Otherwise, an empty string is returned.

Accounting for EXM Email Execution: The Bug and the Workaround

Before reading this section, note that when I first deduced the workaround, I did so by making and then proving/disproving assumptions, in order to "unit test" the method by hand. After doing so and presenting the solution to the OP, we worked together to debug and confirm the hypothesis and identify which assumption case was correct.

When EXM sends out emails it does so without a request context. As such. HttpContext.Current.Request is not set, so the return value of the GetHostName() method will be an empty string.

This is where the logic bug starts to become apparent: the siteInfo.TargetHostName falls back to the siteInfo.HostName so long as it doesn't contain an asterisk, and the result falls back to the HttpContext.Current.Request.Url.Host, which we know to be empty in our execution, since we are executing via EXM.

Look back at the Marker 1 condition that we substituted in the previous section, we can reduce the condition to the following:

siteInfo.Name.Equals(str1, StringComparison.OrdinalIgnoreCase) && 
    hostName.Equals(str4, StringComparison.OrdinalIgnoreCase) ||
    (string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0)

Substituting in for simpler variables, we can equate this to:

A && B || (C || D)

which reduces, like so:

A && B || C || D

and if we set foo = A && B and substitute back in for C and D we get:

foo && string.IsNullOrEmpty(str4) || str4.IndexOf('*') >= 0

The Workaround:

So long as one of the last two expressions is true, it doesn't matter what foo is. Looking back at the GetTargetHostName(...) method, str4 should never be set to the siteInfo.HostName if siteInfo.HostName contains an asterisk, and so - since an asterisk is invalid in a URL's hostname and thus cannot come from the Request.Url.Host (whether or not there is a request context) - unless an asterisk is mistakenly put into the siteInfo.TargetHostName expression D can never be true. This leaves us with expression C. We can ensure that str4 is null or empty, by simply adding an asterisk or a pipe to the siteInfo.HostName, since there is no request context.

The Bug:

The problem here is that the LinkProvider.GetServerUrlElement(...) method does not correctly account for the possibility that the HttpContext could be unavailable. Since this is the case when EXM runs, the LinkProvider.GetServerUrlElement(...) method does not work correctly during EXM email dispatch.

Caveats of the Workaround

The bit caveat with this workaround is that it requires that you do not set the siteInfo.TargetHostName and that you add an asterisk to the hostName. This means that Sitecore will not be able to generate absolute URLs for any links, unless the code that is generating them has access to the HttpContext.

Furthermore, if the site is a multi-site and the code generating the links is supposed to generate them for a different <site> from the current request then the links will be generated for the wrong site. For example, if you are generating a link to the page http://foo.com/some/page and you are making the request from the URL http://bar.com then the resulting link would be http://bar.com/some/page instead of the expected value http://foo.com/some/page.

As such if any of the following are true then this workaround may not work for you:

  • Your site is a multi-site and you sometimes generate links for one site on another (as in the example)
  • Your site generates sitemaps using the siteInfo.TargetHostName
  • Your site generates absolute links using siteInfo.TargetHostName

Note that you can still work around these caveats using custom processors, like the HandleInternalLink processor, or other logic in your code. However, there is no catch-all solution that will handle these caveats or the solution without extending and overriding the native LinkProvider (see the "Alternative Solution"). Unless you decide that you are going to extend and override the native LinkProvider, I do not recommend adding such code as it will basically be a band-aid on a hemorrhage, and Sitecore Support should be able to provide you with the URL.

Next Steps: Sitecore Support

Regardless of whether or not this workaround works for you, it should be only temporary. Reach out to Sitecore Support with the issue details and have them provide you with a patch for the LinkProvider, which you can use as a more permanent solution.

Alternative Solution in Concept

Alternatively, you can create your own LinkProvider to override the Sitecore one, allowing you to fix the bug in the code, directly, without having to deal with the caveats that I described. This is a good bit more involved than it was in previous versions, and I have not tried it myself.

Sitecore now registers the LinkProvider using the DI container included with Sitecore. In order to replace it with your own implementation, you will need to add a new <initialize> processor after the native LinkProvider is registered with the container, remove the native LinkProvider registration in your processor and then register your own. This would be the better solution, but it is still a bit black-box to me, as I have been having trouble finding the processor in which the native LinkProvider is registered. Given that, I have listed it only as an alternative solution.

Even if you do choose to implement this solution, I still recommend that you reach out to Sitecore Support for a patch to serve as a more permanent solution.

2

Dug in a little and came across https://community.sitecore.net/developers/f/10/t/3721

Email Experience Manager 3.3.0 rev. 160527

It's look like a bug for multisite Sitecore environments. For generating url is called standard Sitecore method LinkManager.GetItemUrl, but EXM sends messages not in the main thread and do not have access to HttpContext, so property AlwaysIncludeServerUrl = false doesn't work and url always return server part from targetHostName in site definitions.

As a result I've overridden HandleInternalLink pipeline)

Not sure if it has been fixed in the version you are using or if the second <site> node would be attributing to any of this.

Ultimately I was searching around to see if the difference in the domain between scenario 1 and scenario 2 is flagging the first scenario as external and forcing the appending of the url.

Edit: After digging in further (and the narrative around the other solutions in this post) there looks to possibly be a bug around the execution time of the HandleInternalLinks with regards to the context providing the value for AlwaysIncludeServerUrl.

2
  • Can you describe the bug or what causes it? Commented Mar 14, 2017 at 19:04
  • @ZacharyKniebel I am working on creating a specific test to try and narrow it down, at this point I am focusing on the SiteContext when the two scenarios execute. Trying to come up with more concrete answers.
    – vandsh
    Commented Mar 14, 2017 at 19:36
0

I would recommend trying to force a correct url for the Renderer Url. To do this create a new pipeline processor as below.

namespace MySolution.Initialize
{
    public class EXM
    {
        public void Process(PipelineArgs args)
        {
            GlobalSettings.RendererUrl = Settings.GetSetting("EXM.RendererUrl");
        }
    }
}

And then patch the pipeline processor

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="MySolution.Initialize.EXM, MySolution"/>
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

This will make sure you always are using a specific url to generate from.

1
  • While this appears to be an interesting workaround, it doesn't answer my main question which is, why does Link Manager return different results for what is apparently the same inputs. Commented Mar 14, 2017 at 15:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.