spring-projects / spring-projects/spring-framework

Doc: LocaleContextHolder with inheritable only propagates to newly created threads

Open
#36,687 5 comments 0 reactions 1 assignee View on GitHub

@jhoeller is already working on this.

Since Apr 22, 2026.

in: core type: documentation
Dominant language
Java
Stars
60.2k
Forks
38.8k
Avg merge
5d 2h
Merged PRs (30d)
27

Description

I noticed that the Locales when trying to use them inside threads are not consistent when using LocaleContextHolder.setLocale(Locale.GERMAN, true);.
It seems like the thread pools use the language it once was sent and uses it for all other requests.
You can reporduce it when using a threadpool of 1.

This is the Test I have:

@SpringBootTest
class AsyncContextPropagationTest {

    private ThreadPoolTaskExecutor delegate;
    private TaskExecutor taskExecutor;

    @BeforeEach
    void setUpExecutor() {
        delegate = new ThreadPoolTaskExecutor();
        delegate.setCorePoolSize(1);
        delegate.setMaxPoolSize(100);
        delegate.setQueueCapacity(50);
        delegate.setThreadNamePrefix("test-async-");
        delegate.initialize();

        taskExecutor = new DelegatingSecurityContextAsyncTaskExecutor(delegate);
    }

    @AfterEach
    void tearDownExecutor() {
        if (delegate != null) {
            delegate.shutdown();
        }
    }

    @Test
    void localeIsVisibleInAsyncTask() {
        LocaleContextHolder.setLocale(Locale.GERMAN, true);
        CompletableFuture<Locale> futureGerman = CompletableFuture.supplyAsync(
            LocaleContextHolder::getLocale,
            taskExecutor
        );
        var futureGermanResult = futureGerman.join();

        LocaleContextHolder.setLocale(Locale.ENGLISH, true);
        CompletableFuture<Locale> futureEnglish = CompletableFuture.supplyAsync(
            LocaleContextHolder::getLocale,
            taskExecutor
        );

        var futureEnglishResult = futureEnglish.join();

        assertThat(futureGermanResult).isEqualTo(Locale.GERMAN);
        assertThat(futureEnglishResult).isEqualTo(Locale.ENGLISH);
    }
}

Right now the workaround is to use an explicit TaskDecorator for that case to copy the locale to all child threads.

public class ContextCopyingDecorator implements TaskDecorator {

    @NonNull
    @Override
    public Runnable decorate(@NonNull Runnable runnable) {
        var locale = LocaleContextHolder.getLocale();

        return () -> { // code runs inside executor thread and binds context
            try {
                if (locale != null) {
                    LocaleContextHolder.setLocale(locale);
                }
                runnable.run();
            } finally {
                // Nothing to do.
            }
        };
    }
}

Then set it via delegate.setTaskDecorator(new ContextCopyingDecorator()); and it works just fine.

Not sure if the inheritable on setLocale() is intended to work like that or its simply a bug.
At least I don't know why it would work like that.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.