Close

Spring MVC - Obtaining Client Time Zone Information

[Last Updated: Sep 4, 2026]

When a client sends an HTTP request to the server, the request header is automatically populated with 'Accept-Language', which helps the server side determine the client's Locale. This header doesn't carry any client time zone information. There's currently no standard HTTP header or specification for obtaining a client's time zone information.



Spring support for Time zone

Definition of LocaleContextResolver

Version: 7.0.6
 package org.springframework.web.servlet;
 public interface LocaleContextResolver extends LocaleResolver {
     LocaleContext resolveLocaleContext(HttpServletRequest request); 1
     void setLocaleContext(HttpServletRequest request, 2
                           @Nullable HttpServletResponse response,
                           @Nullable LocaleContext localeContext);
     default Locale resolveLocale(HttpServletRequest request); 3
     default void setLocale(HttpServletRequest request, 4
                            @Nullable HttpServletResponse response,
                            @Nullable Locale locale);
 }
1Resolve the current locale context via the given request.
2Set the current locale context to the given one, potentially including a locale with associated time zone information.
3Default implementation of LocaleResolver#resolveLocale(HttpServletRequest) that delegates to #resolveLocaleContext(HttpServletRequest), falling back to HttpServletRequest#getLocale() if necessary. (Since 6.0)
4Default implementation of LocaleResolver#setLocale(HttpServletRequest, that delegates to #setLocaleContext(HttpServletRequest,, using a SimpleLocaleContext. (Since 6.0)

Definition of TimeZoneAwareLocaleContext

Version: 7.0.6
 package org.springframework.context.i18n;
 public interface TimeZoneAwareLocaleContext extends LocaleContext {
     TimeZone getTimeZone(); 1
 }
1Return the current TimeZone, which can be fixed or determined dynamically, depending on the implementation strategy.

The LocaleContextResolver interface may include time zone information. Spring currently provides three implementations of this interface: CookieLocaleResolver, FixedLocaleResolver and SessionLocaleResolver. In the previous four tutorials, we saw how to use them to obtain the client's Locale. Any of these classes can be used to set the time zone via this method:

void setLocaleContext(HttpServletRequest request,
                      HttpServletResponse response,
                      LocaleContext localeContext)

Handler method support

A controller's handler method can have java.util.TimeZone or java.time.ZoneId parameters. These parameters are populated with corresponding values as determined by the active LocaleContextResolver. If no time zone is set via LocaleContextResolver#setLocaleContext() then the system's default time zone will be used. Note that this fallback only applies to handler method parameters: directly calling LocaleContextResolver#resolveLocaleContext() and reading its time zone will return null unless a time zone was explicitly set beforehand via LocaleContextResolver#setLocaleContext().

The following snippet is from ServletRequestMethodArgumentResolver.java which resolves the handler method arguments:

.....
 public Object resolveArgument( ...... ) throws Exception {
   .....
    else if (TimeZone.class == paramType) {
     TimeZone timeZone = RequestContextUtils.getTimeZone(request);
     return (timeZone != null ? timeZone : TimeZone.getDefault());
    }
    else if (ZoneId.class == paramType) {
     TimeZone timeZone = RequestContextUtils.getTimeZone(request);
     return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
    }
   .....
  }
 ...

The following handler will always print the system's (the server computer's) time zone and zone id, regardless of what time zone the client is in:

@RequestMapping("/test")
public void exampleHandler (TimeZone tz, ZoneId zid) {
    System.out.println(tz);
    System.out.println(zid);
}


So how to obtain client time zone?

One solution for obtaining the client's time zone is to use client-side JavaScript's Date API:

<script>
 var date = new Date()
 var offset = date.getTimezoneOffset()
</script>

The browser then sends this offset from JavaScript to the server side so that Spring can apply it via LocaleContextResolver#setLocaleContext(). Let's see an example of how to achieve that.

Note: getTimezoneOffset() returns the number of minutes behind UTC (positive west of UTC), which is the opposite sign convention from a standard UTC offset — that's why the example below negates it when constructing a ZoneOffset. Also, because this API returns only a numeric offset, it cannot distinguish between zones that currently share the same offset, and it doesn't carry daylight-saving rules the way an IANA zone id does. In modern browsers, Intl.DateTimeFormat().resolvedOptions().timeZone is generally the better choice, since it returns an actual IANA zone id (e.g. "America/Chicago") instead of a raw offset. The offset-based approach is kept here because it's what this example's server-side flow is built around.



Example


Creating a custom LocaleContextResolver

A custom LocaleContextResolver implementation will delegate Locale responsibilities to AcceptHeaderLocaleResolver and time zone responsibility to one of Spring's other implementations of LocaleContextResolver:

package com.logicbig.example;

import org.springframework.context.i18n.LocaleContext;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Locale;

public class AcceptHeaderLocaleTzCompositeResolver implements LocaleContextResolver {
    private LocaleContextResolver localeContextResolver;
    private AcceptHeaderLocaleResolver acceptHeaderLocaleResolver;

    public AcceptHeaderLocaleTzCompositeResolver (LocaleContextResolver localeContextResolver) {
        this.localeContextResolver = localeContextResolver;
        acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
        acceptHeaderLocaleResolver.setDefaultLocale(Locale.getDefault());
    }

    @Override
    public LocaleContext resolveLocaleContext (HttpServletRequest request) {
        return localeContextResolver.resolveLocaleContext(request);
    }

    @Override
    public void setLocaleContext (HttpServletRequest request, HttpServletResponse response,
                                  LocaleContext localeContext) {
        localeContextResolver.setLocaleContext(request, response, localeContext);

    }

    @Override
    public Locale resolveLocale (HttpServletRequest request) {
        return acceptHeaderLocaleResolver.resolveLocale(request);
    }

    @Override
    public void setLocale (HttpServletRequest request, HttpServletResponse response, Locale locale) {
        acceptHeaderLocaleResolver.setLocale(request, response, locale);

    }
}

Creating a custom HandlerInterceptor

A custom HandlerInterceptor implementation will intercept each request and try to obtain a TimeZone instance. If it is null, the request is forwarded to a handler.

Note: HandlerInterceptorAdapter was deprecated in Spring 5.3 and removed in Spring 6. Since HandlerInterceptor's methods are now default methods on the interface itself, you can implement HandlerInterceptor directly, as shown below.

package com.logicbig.example;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.support.RequestContextUtils;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.TimeZone;

public class TzRedirectInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle (HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {

        TimeZone tz = RequestContextUtils.getTimeZone(request);

        if (tz == null) {
            System.out.println("Forwarding to js to get timezone offset");
            request.setAttribute("requestedUrl", request.getRequestURI());
            RequestDispatcher dispatcher = request.getRequestDispatcher("/tzHandler");
            dispatcher.forward(request, response);
            return false;
        }

        return true;
    }
}

RequestContextUtils is a helper class. The call above gets the time zone from the underlying LocaleContextResolver.



The handler method with '/tzHandler' mapping:

The above interceptor forwards the request to this handler.

@Controller
public class TzExampleController {
    .............
    @GetMapping("/tzHandler")
    public String handle() {
        return "tzJsPage";
    }
    .............
}


tzJsPage.jsp

This contains JavaScript to obtain the client's time zone offset. This page directly redirects to another URI /tzValueHandler without any user interaction:

src/main/webapp/WEB-INF/views/tzJsPage.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>

<html>
 <body>
    <form method="post" id="tzForm" action="/tzValueHandler">
      <input id="tzInput" type="hidden" name="timeZoneOffset"><br>
      <input type="hidden" name="requestedUrl" value="${requestedUrl}">
    </form>

    <script>
        var date = new Date();
        var offSet = date.getTimezoneOffset();
        document.getElementById("tzInput").value = offSet;
        document.getElementById("tzForm").submit();
    </script>

 </body>
</html>




The handler method with '/tzValueHandler' mapping

This handler sets the time zone instance on the underlying LocaleContextResolver:

package com.logicbig.example;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Locale;
import java.util.TimeZone;

@Controller
public class TzExampleController {
    .............
    @PostMapping(value = "/tzValueHandler")
    public String handleTzValue(
            Locale locale,
            HttpServletRequest req,
            HttpServletResponse res,
            @RequestParam("requestedUrl") String requestedUrl,
            @RequestParam("timeZoneOffset") int timeZoneOffset) {

        ZoneOffset zoneOffset =
                ZoneOffset.ofTotalSeconds(-timeZoneOffset * 60);

        TimeZone timeZone = TimeZone.getTimeZone(zoneOffset);

        LocaleContextResolver localeResolver =
                (LocaleContextResolver) RequestContextUtils.getLocaleResolver(
                        req);

        localeResolver.setLocaleContext(req, res,
                                        new SimpleTimeZoneAwareLocaleContext(
                                                locale, timeZone));

        return "redirect:" + requestedUrl;

    }
}

Handler method for testing

package com.logicbig.example;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.LocaleContextResolver;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Locale;
import java.util.TimeZone;

@Controller
public class TzExampleController {

    @GetMapping("/")
    @ResponseBody
    public String testHandler(Locale clientLocale,
                              ZoneId clientZoneId) {

        ZoneOffset serverZoneOffset = ZoneOffset.ofTotalSeconds(
                TimeZone.getDefault().getRawOffset() / 1000);

        return String.format("client timeZone: %s" +
                                     "<br/> " +
                                     "server timeZone: %s" +
                                     "<br/>" +
                                     " locale: %s%n",
                             clientZoneId.normalized().getId(),
                             serverZoneOffset.getId(),
                             clientLocale);
    }
    .............
}

Configuration

@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    LocaleContextResolver localeResolver() {
        SessionLocaleResolver l = new SessionLocaleResolver();
        AcceptHeaderLocaleTzCompositeResolver r = new
                AcceptHeaderLocaleTzCompositeResolver(l);
        return r;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        TzRedirectInterceptor interceptor = new TzRedirectInterceptor();
        InterceptorRegistration i = registry.addInterceptor(interceptor);
        i.excludePathPatterns("/tzHandler", "/tzValueHandler");
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
    registry.jsp("/WEB-INF/views/", ".jsp");
    }
}


Running the example

To try examples, run embedded Jetty (configured in pom.xml of example project below):

mvn jetty:run

Since in a development environment both client and server are most likely running on the same computer, we'll use the Firefox browser, which allows changing the browser's time zone via an environment variable. Run cmd.exe:

C:\Program Files\Mozilla Firefox>set TZ=EST5EDT

C:\Program Files\Mozilla Firefox>firefox.exe

C:\Program Files\Mozilla Firefox>

In the console where we ran the Jetty server, a line will be printed:
'Forwarding to js to get timezone offset'
This line is printed by our interceptor. Refreshing the same page multiple times won't print the same line again, because on further access the time zone is retrieved from the HttpSession, and the request is no longer forwarded to the JavaScript logic.




Using CookieLocaleResolver and FixedLocaleResolver

In the example above we used SessionLocaleResolver (as the LocaleContextResolver delegate specific to time zone handling) with our AcceptHeaderLocaleTzCompositeResolver. The server side will remember the time zone set by the JavaScript logic until the end of the session. If we want it remembered for longer or shorter than the session's lifetime, we should use CookieLocaleResolver for time zone handling.

If we want to use a fixed time zone instead of the system's time zone, we should use FixedLocaleResolver with our AcceptHeaderLocaleTzCompositeResolver; in that case we won't need our custom TzRedirectInterceptor and JavaScript logic. The fixed time zone value is set at registration time in the main class.

Across all the options just mentioned, the locale selection strategy remains the same in our example, i.e. AcceptHeaderLocaleResolver. If we want to change that too, we don't need a composite LocaleContextResolver — a single LocaleContextResolver will do the job, unless we want to apply different strategies for locale and time zone separately. For locale handling with a resolver other than AcceptHeaderLocaleResolver, we have to provide a custom locale selection as well, as shown in this and this example. The time zone can also be selected via a user interface instead of using the JavaScript Date API.


Integration Test

package com.logicbig.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

@SpringJUnitWebConfig(WebConfig.class)
public class ClientTimeZoneTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvcTester mockMvcTester;

    @BeforeEach
    public void setup() {
        mockMvcTester = MockMvcTester.from(webApplicationContext);
    }

    @Test
    public void firstRequestWithNoTimeZoneGetsForwardedToJsHandler() {
        assertThat(mockMvcTester.get()
                                .uri("/")
                                .session(new MockHttpSession())
                                .exchange())
                .hasStatusOk()
                .hasForwardedUrl("/tzHandler");
    }

    @Test
    public void timeZonePersistsInSessionAcrossRequests() {
        MockHttpSession session = new MockHttpSession();

        assertThat(mockMvcTester.post()
                                .uri("/tzValueHandler")
                                .session(session)
                                .param("timeZoneOffset", "120")
                                .param("requestedUrl", "/")
                                .exchange())
                .hasStatus3xxRedirection()
                .hasRedirectedUrl("/");

        assertThat(mockMvcTester.get().uri("/")
                                .session(session)
                                .exchange())
                .hasStatusOk()
                .bodyText().contains("client timeZone: -02:00");
    }
}

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 4.3.0.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 6.2.18
    • 6.2.19
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    Versions in green have been tested.

  • spring-test 7.0.6 (Spring TestContext Framework)
  • junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
  • jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
  • hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
  • assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
  • JDK 25
  • Maven 3.9.11

Spring MVC - Obtaining Client Time Zone Information Select All Download
  • spring-mvc-timezone
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • TzExampleController.java
          • webapp
            • WEB-INF
              • views
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join