Viewer doesn't load document with lot of 404s for /get-page calls

Hi, Just upgraded to 8.0.7. and our site is hosted on azure app service, and it’s behind gateway.
so instead of “https://sample.com” host name for “get-page” call is “https://sample.azurewebsites.net

Did bit more investigation on this. please find details below.

At present groupdocs request of “/view-data” when noticed from browser’s network window returns “pageUrl” whose hostname which is the one internal of cloudprovider. It doesn’t use the one user has put in the browser, which result in 404, as site access is blocked over internal host name.

e.g. instead of https://sample.com it uses https://sample.azurewebsites.net which is the nearest/internal hostname of azure.

Sample output of view-data call to ViewerController action method.
{
“file”: “3477/dummy.pdf”,
“fileType”: “pdf”,
“fileName”: “dummy.pdf”,
“canPrint”: true,
“searchTerm”: “”,
“pages”: [
{
“number”: 1,
“width”: 595,
“height”: 842,
“pageUrl”: “https://sample.azurewebsites.net/viewer-api/get-page?file=3477%2Fdummy.pdf&page=1”,
“thumbUrl”: null
},
{
“number”: 2,
“width”: 595,
“height”: 842,
“pageUrl”: “https://sample.azurewebsites.net/viewer-api/get-page?file=3477%2Fdummy.pdf&page=2”,
“thumbUrl”: null

Expected: may be pageurl can be returned as without hostname “/viewer-api/get-page?file=3477%2fdummy.pdf&page=1” and while making call use “/” to be relative to the entered urls hostname from client side.

We can use ApiDomain setting but that ties to single domain url, so not a perfect solution. also we don’t know the port number until request has arrived, so urls with port numbers also can’t be set statically with this setting.

Invalid (assume user is browsing over sample.com)
https://sample.azurewebsites.net/viewer-api/get-page?file=3477%2Fbdummy.pdf&page=1

Valid (assume user is browsing over sample.com)
https://sample.com/viewer-api/get-page?file=3477%2Fdummy.pdf&page=1

Please can this be fixed it wasn’t there with 6.x version.

Thanks

Sachin

@vladimir.litvinchik fyi…

@sachinerande

Thank you for sharing your use case which clarifies what is expected behavior. I thought that it is supported but it appears that it does not work with combination of options. Anyway, at the moment you can implement IApiUrlBuilder to handle URL building by yourself. The following code shows how it can be implemented to make URL relative:

using System.Web;
using GroupDocs.Viewer.UI.Api.Configuration;
using GroupDocs.Viewer.UI.Api.Utils;

var builder = WebApplication.CreateBuilder(args);
builder.Services
        .AddGroupDocsViewerUI();

builder.Services
        .AddControllers()
        .AddGroupDocsViewerSelfHostApi(config =>
        {
            config.SetLicensePath("c://licenses//GroupDocs.Viewer.lic");
        })
        .AddLocalStorage("./Files")
        .AddLocalCache("./Cache");

// NOTE: make sure to place it after AddGroupDocsViewerSelfHostApi method,
// since it registers the default implmentation and we have to override it
builder.Services.AddTransient<IApiUrlBuilder, MyApiUrlBuilder>();

var app = builder.Build();
app
    .UseRouting()
    .UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", (HttpContext context) =>
        {
            context.Response.ContentType = "text/html";
            return "<a href='/viewer?file=annual-review.docx'>Open annual-review.docx file</a>";
        });

        endpoints.MapGroupDocsViewerUI(options =>
        {
            options.UIPath = "/viewer";
            options.ApiEndpoint = "/viewer-api";
        });
        endpoints.MapGroupDocsViewerApi(options =>
        {
            options.ApiDomain = "/";
            options.ApiPath = "/viewer-api";
        });
    });

app.Run();


public class MyApiUrlBuilder : IApiUrlBuilder
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly Options _options;

    public MyApiUrlBuilder(
        IHttpContextAccessor httpContextAccessor,
        IOptionsProvider optionsProvider)
    {
        _httpContextAccessor = httpContextAccessor;
        _options = optionsProvider.GetOptions();
    }

    public string GetApiDomainOrDefault()
    {
        var request = _httpContextAccessor.HttpContext.Request;

        if (string.IsNullOrEmpty(_options.ApiDomain))
        {
            var baseUrl = $"{request.Scheme}://{request.Host}";

            if (!string.IsNullOrEmpty(request.PathBase))
            {
                baseUrl += request.PathBase.Value;
            }

            return baseUrl;
        }

        return _options.ApiDomain;
    }

    public string BuildPageUrl(string file, int page, string extension) =>
        BuildUrl(
            apiDomain: GetApiDomainOrDefault(),
            apiPath: _options.ApiPath,
            apiMethodName: GroupDocs.Viewer.UI.Api.ApiNames.API_METHOD_GET_PAGE,
            values: new { file = file, page = page });

    public string BuildThumbUrl(string file, int page, string extension) =>
        BuildUrl(
            apiDomain: GetApiDomainOrDefault(),
            apiPath: _options.ApiPath,
            apiMethodName: GroupDocs.Viewer.UI.Api.ApiNames.API_METHOD_GET_THUMB,
            values: new { file = file, page = page });

    public string BuildPdfUrl(string file) =>
        BuildUrl(
            apiDomain: GetApiDomainOrDefault(),
            apiPath: _options.ApiPath,
            apiMethodName: GroupDocs.Viewer.UI.Api.ApiNames.API_METHOD_GET_PDF,
            values: new { file = file });

    public string BuildResourceUrl(string file, int page, string resource) =>
        BuildUrl(
            apiDomain: GetApiDomainOrDefault(),
            apiPath: _options.ApiPath,
            apiMethodName: GroupDocs.Viewer.UI.Api.ApiNames.API_METHOD_GET_RESOURCE,
            values: new { file = file, page = page, resource = resource });

    public string BuildResourceUrl(string file, string pageTemplate, string resourceTemplate) =>
        BuildUrl(
            apiDomain: GetApiDomainOrDefault(),
            apiPath: _options.ApiPath,
            apiMethodName: GroupDocs.Viewer.UI.Api.ApiNames.API_METHOD_GET_RESOURCE,
            values: new { file = file, page = pageTemplate, resource = resourceTemplate });

    /// <summary>
    /// Builds a relative URL ignoring doiman name and api path
    /// <param name="apiDomain">The base API domain, e.g., "https://www.example.com".</param>
    /// <param name="apiPath">The API path, e.g., "viewer-api".</param>
    /// <param name="apiMethodName">The API method name, e.g., "get-page".</param>
    /// <param name="values">An object containing query parameter key-value pairs, e.g., new { file = "my-file.docx", page = 5 }.</param>
    /// <returns>The relative URL as a string, e.g., "/get-page?file=my-file.docx&page=5".</returns>
    /// <example>
    /// string url = UrlHelper.BuildUrl("https://www.example.com", "viewer-api", "get-page", new { file = "my-file.docx", page = 5 });
    /// Console.WriteLine(url); // Output: /get-page?file=my-file.docx&page=5
    /// </example>
    private static string BuildUrl(string apiDomain, string apiPath, string apiMethodName, object values)
    {
        if (string.IsNullOrWhiteSpace(apiDomain))
            throw new ArgumentNullException(nameof(apiDomain), "API domain cannot be null or empty.");

        if (string.IsNullOrWhiteSpace(apiPath))
            throw new ArgumentNullException(nameof(apiPath), "API path cannot be null or empty.");

        if (string.IsNullOrWhiteSpace(apiMethodName))
            throw new ArgumentNullException(nameof(apiMethodName), "API method name cannot be null or empty.");

        // Ensure proper URL formatting
        string basePath = $"/{apiMethodName.TrimStart('/')}";
        var queryString = BuildQueryString(values);

        return string.IsNullOrWhiteSpace(queryString) ? basePath : $"{basePath}?{queryString}";
    }

    private static string BuildQueryString(object values)
    {
        if (values == null)
            return string.Empty;

        var queryParameters = new List<string>();
        foreach (var property in values.GetType().GetProperties())
        {
            var key = property.Name;
            var value = property.GetValue(values, null);

            if (value != null)
            {
                queryParameters.Add($"{HttpUtility.UrlEncode(key)}={HttpUtility.UrlEncode(value.ToString())}");
            }
        }

        return string.Join("&", queryParameters);
    }
}

The sample app: sample-app.zip (3.7 MB)

And it here is view-data method response:

I have created a ticket, internal ID is VIEWERNET-5377 to address this issue in the future version.

Hi @vladimir.litvinchik, implementation for IApiUrlBuilder has worked. For now added it and it fixes the issue. I think the best way is to have it via next upgrade, so we don’t need to maintain the file. Please notify whenever the next nuget would be available.

Thank you!

@sachinerande

Thank you for the feedback! We’ll update you here when the issue is fixed.

1 Like