5-Minute TLS/SSL Troubleshooting Playbook - IP-direct access only (curl / openssl)

 

🧑🏻‍💻 Introduction

When you go through DNS, you can be misled by:

  • caching
  • load balancers / CDNs
  • name-resolution mistakes

This guide standardizes all commands to IP-direct access + correct SNI so you can isolate the real cause quickly.

 

🧑🏻‍💻 Prerequisite Variables


DOMAIN=example.com 
IP=1.2.3.4

 

🧑🏻‍💻 Overall Flow


① Check reachability with curl (IP direct) 
    ↓ 
② Read certificate verification result 
    ↓ 
③ Get raw TLS data with openssl 
    ↓ 
④ Check certificate expiration 
    ↓ 
⑤ Verify SAN 
    ↓ 
⑥ Check intermediate certificate 
    ↓ 
⑦ Verify TLS versions

 

🧑🏻‍💻 ① HTTP Reachability (IP direct + SNI)


curl -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null

OK


* Connected to example.com (1.2.3.4) port 443
* SSL certificate verify ok.
< HTTP/1.1 200 OK

Failure


Connection refused

  • nginx / apache not running
  • closed port
  • firewall

 

🧑🏻‍💻 ③ Raw TLS Layer Information


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN

OK


CONNECTED(00000003)
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)

 

🧑🏻‍💻 ④ Certificate Expiration


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN 2>/dev/null \
 | openssl x509 -noout -dates


notAfter=May 2 23:59:59 2026 GMT

 

🧑🏻‍💻 ⑤ SAN (Domain Match)


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN \
 | openssl x509 -noout -ext subjectAltName


DNS:example.com
DNS:www.example.com

 

🧑🏻‍💻 ⑥ Missing Intermediate Certificate Check


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN \
 -showcerts

OK


Certificate chain
 0 s:CN = example.com
 1 s:C = US, O = Let's Encrypt, CN = R3

Missing


Certificate chain
 0 s:CN = example.com

→ fullchain.pem not configured

 

🧑🏻‍💻 ⑦ TLS Version Restrictions

TLS 1.2


curl --tlsv1.2 -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null

TLS 1.3


curl --tlsv1.3 -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null


unsupported protocol

→ ssl_protocols misconfiguration

 

🧑🏻‍💻 ⑧ Detect SNI Misconfiguration (intentionally omit it)


openssl s_client -connect $IP:443


subject=CN = default.example.net

→ default certificate returned
→ virtual host configuration issue

 

🧑🏻‍💻 Copy-Paste 5-Minute Diagnosis Set



DOMAIN=example.com 
IP=1.2.3.4 

curl -v https://$IP \
 -H "Host: $DOMAIN"\
 -o /dev/null 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN -brief 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN 2>/dev/null \
 | openssl x509 -noout -dates 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN \
 | openssl x509 -noout -ext subjectAltName

 

🧑🏻‍💻 Root-Cause Shortcut Map


Cannot connect even with IP direct
 → server or firewall 

Verify error 
 → intermediate certificate 

Expired
 → certificate renewal missed

SAN mismatch
 → wrong certificate selected 

Different cert without SNI
 → virtual host configuration 

Only one of TLS1.2 / 1.3 fails
 → protocol restriction

 

🧑🏻‍💻 Summary

By eliminating DNS and fixing:

  • IP-direct access
  • correct SNI

your TLS troubleshooting speed improves dramatically.

This workflow is ready to copy-paste in real incidents.

👉 openssl-s_client - OpenSSL Documentation
👉 curl - SSL CA Certificates


[Jetpack Compose] Implement "Pull-to-Refresh" with the New PullToRefreshBox

The "Pull-to-Refresh" gesture is a staple in Android app UI.

While we previously relied on Modifier.pullRefresh, Jetpack Compose has introduced PullToRefreshBox in Material 3 as the new standard. It's more intuitive and requires much less boilerplate code.

In this post, we’ll quickly cover everything from basic implementation to customization!

 

🧑🏻‍💻 1. Prerequisites

PullToRefreshBox is available in Material 3 (version 1.3.0 or later).

Make sure to check your build.gradle dependencies:


dependencies {
    implementation("androidx.compose.material3:material3:1.3.0")
}

 

🧑🏻‍💻 2. Basic Implementation Pattern

The best part about PullToRefreshBox is that it encapsulates both the refresh logic and the indicator UI into a single component.


@Composable
fun RefreshableListScreen() {
    var isRefreshing by remember { mutableStateOf(false) }
    val scope = rememberCoroutineScope()
    val items = remember { mutableStateListOf("Initial Item A", "Initial Item B") }

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = {
            scope.launch {
                isRefreshing = true
                // Perform your refresh logic (e.g., API calls)
                delay(2000) 
                items.add(0, "New Item ${items.size + 1}")
                isRefreshing = false
            }
        }
    ) {
        LazyColumn(Modifier.fillMaxSize()) {
            items(items) { item ->
                ListItem(headlineContent = { Text(item) })
            }
        }
    }
}

Key Highlights

  • isRefreshing: A boolean that controls the visibility of the refresh indicator.
  • onRefresh: The callback triggered when the user performs the pull gesture.
  • Content Size: Ensure your scrollable content (like LazyColumn) uses Modifier.fillMaxSize() so the pull gesture is detectable across the entire area.

 

🧑🏻‍💻 3. Practical Usage with ViewModel

In a production environment, it's best practice to let a ViewModel handle the state.


class MyViewModel : ViewModel() {
    var isRefreshing by mutableStateOf(false)
        private set

    fun refreshData() {
        viewModelScope.launch {
            isRefreshing = true
            // Simulate network call
            isRefreshing = false
        }
    }
}

val viewModel: MyViewModel = viewModel()
PullToRefreshBox(
    isRefreshing = viewModel.isRefreshing,
    onRefresh = { viewModel.refreshData() }
) {
    // ... Content
}

 

🧑🏻‍💻 4. Customizing the Design

If you want to change the indicator's color to match your brand, use the indicator parameter.


PullToRefreshBox(
    isRefreshing = isRefreshing,
    onRefresh = { /* ... */ },
    indicator = {
        PullToRefreshDefaults.Indicator(
            state = it,
            isRefreshing = isRefreshing,
            containerColor = Color.DarkGray, // Background color
            color = Color.Cyan              // Progress spinner color
        )
    }
) {
    // ...
}

 

🧑🏻‍💻 Conclusion: Simplified Refresh Logic

With the arrival of PullToRefreshBox, implementing this common UI pattern has never been easier.

  • Use Material 3 1.3.0+.
  • Pass the state (isRefreshing).
  • Handle the logic in onRefresh.

That’s it! You now have a modern, native-feeling refresh experience.


Modernizing Android Build Scripts: Moving from "android { ... }" to "configure { ... }"

In the world of Android development, Kotlin DSL has become the standard for writing build scripts.

While the familiar android { ... } block works perfectly for simple projects, as your project grows and you start sharing build logic across multiple modules (e.g., using Convention Plugins), you might find it a bit limiting.

Today, we’ll look at why and how to switch to the more explicit and scalable configure<ApplicationExtension> syntax.

 

🧑🏻‍💻 1. Why Make the Switch?

The standard android { ... } block in build.gradle.kts is actually a "shorthand" provided by the Android Gradle Plugin (AGP). While convenient, using configure<T> offers several advantages:

  • Better Type Safety: By explicitly telling Gradle that "this block is an ApplicationExtension," the IDE (Android Studio) can provide more accurate code completion and error highlighting.
  • Scalable Build Logic: If you are moving common logic into buildSrc or external plugins to keep your Gradle files DRY (Don't Repeat Yourself), using the explicit extension type becomes essential for writing clean, reusable functions.

 

🧑🏻‍💻 2. The Transformation: Before vs. After

Let’s compare the standard approach with the explicit configuration style for an App module.

Before: The Standard android Block


// app/build.gradle.kts
android {
    compileSdk = 35
    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 26
        targetSdk = 35
    }
}

After: Using configure<ApplicationExtension>
Note that you will need to import the ApplicationExtension class explicitly.


// app/build.gradle.kts
import com.android.build.api.dsl.ApplicationExtension

configure<ApplicationExtension> {
    compileSdk = 35
    defaultConfig {
        applicationId = "com.example.myapp"
        minSdk = 26
        targetSdk = 35
        // ...
    }
}

 

🧑🏻‍💻 3. Choosing the Right Extension Type

Not every module is an "Application."

You should choose the extension type that matches your module's purpose:

[!TIP]
Use CommonExtension when writing shared logic that applies to both your App and Library modules (like Java versioning or Compose options).

 

🧑🏻‍💻 4. Practical Implementation: Reusable Build Logic

The true power of this syntax shines when you extract common configurations into a function, such as in buildSrc.


// Example of a shared configuration function in buildSrc
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Project

fun Project.configureAndroidApplication() {
    extensions.configure<ApplicationExtension> {
        compileSdk = 35
        defaultConfig {
            minSdk = 26
            // ...other shared settings
        }
    }
}

By defining your build logic this way, your module-level Gradle files stay thin and highly maintainable.

 

🧑🏻‍💻 Conclusion

The traditional android { ... } block is great for its brevity. However, once your project reaches a certain scale and you start treating your build configuration as "real code," switching to configure is the way to go.

It brings better IDE support, type safety, and makes your build logic much easier to share across modules.


Troubleshooting SQLDelight: The "KotlinSourceSet 'main' not found" Error in AGP 9

If you've recently tried jumping onto the bleeding edge with Android Gradle Plugin (AGP) 9.0, you might have hit a brick wall during project sync. A common culprit popping up lately is a cryptic error from SQLDelight:


KotlinSourceSet with name 'main' not found.

This issue, tracked under SQLDelight #6078, highlights a significant shift in how Android and Kotlin interact in the latest build tools. Here’s the breakdown of what's happening and how to fix it.

👉 KotlinSourceSet with name 'main' not found with AGP9 · Issue #6078 · sqldelight/sqldelight

 

🧑🏻‍💻 The Problem: Why is 'main' Missing?

The conflict arises because AGP 9 introduces a "New DSL" and changes how Kotlin source sets are managed. Historically, SQLDelight’s Gradle plugin looked for a source set explicitly named "main" to inject its generated code.

However, in AGP 9 (especially with the builtInKotlin flag enabled), the way source sets are registered has changed. The legacy "main" container that SQLDelight expects is either missing or hidden, causing the configuration phase to fail immediately.

 

🧑🏻‍💻 The Discussion Flow: From Discovery to Workaround

The GitHub thread reveals an interesting evolution of the fix:

  • The Discovery: Early adopters reported that even disabling the experimental newDsl didn't fix the crash.
  • The Culprit: Contributors identified that the SQLDelight plugin was making "unsafe" assumptions about the existence of the "main" source set.
  • The Temporary Fix: A specific flag in gradle.properties was found to restore the old behavior, allowing the plugin to find what it needs.

 

🧑🏻‍💻 How to Fix It (The Workaround)

Until the official patches in SQLDelight (specifically PR #6079 and #6091) are fully merged and released, you can unblock your development by adding these flags to your gradle.properties file:


# Temporary fix for SQLDelight + AGP 9
android.newDsl=false
android.builtInKotlin=true
android.disallowKotlinSourceSets=false

The key line here is android.disallowKotlinSourceSets=false. This tells AGP 9 to allow the traditional Kotlin source set structures that SQLDelight currently relies on.

 

🧑🏻‍💻 The Road Ahead: Permanent Fixes

The maintainers (including Jake Wharton and the CashApp team) are already working on a long-term solution. The goal is to move away from searching for "main" and instead use the proper AGP/Kotlin API to register generated code.

  • PR #6079: Focuses on implementing a more robust schema configuration.
  • PR #6091: Modernizes the plugin to play nice with the AGP 9 New DSL.

The takeaway?

AGP 9 is a major shift. If you’re using third-party plugins that generate code (like SQLDelight or Wire), expect a few bumps in the road as these libraries catch up to the new Gradle architecture.


Fix "InitializationProvider" Error in the AGP 9 Era 🚀

Hi fellow Android devs! 🤖

You’ve finished your app, everything works perfectly in Debug mode, and you’re finally ready to hit that "Release" button. But then… CRASH. 💥

You look at the logs and see this scary message:


Fatal Exception: java.lang.RuntimeException: 
  Unable to get provider androidx.startup.InitializationProvider

Don't worry! You’re not alone, and your code isn't "broken." You've just run into a little disagreement between WorkManager and R8 (the code shrinker), especially if you're using the latest AGP 9 (Android Gradle Plugin).

Let’s fix it together in 3 minutes! ☕️

 

What’s happening under the hood? 🧐

When you set isMinifyEnabled = true for your release build, a smart tool called R8 starts cleaning up your code. It looks for anything "unused" and removes it to make your app tiny.

However, WorkManager (the tool that handles background tasks for things like AdMob or Firebase) has a little secret: it uses a class called WorkDatabase_Impl.

The problem? R8 doesn't see anyone "calling" this class in your code, so it thinks, "Hey, this is trash!" and throws it away. When your app starts, WorkManager looks for its database class, finds nothing, and—BOOM—the app crashes.

With AGP 9, R8 is stricter than ever, so we have to be very clear about what we want to keep.

You don't even need to update your library versions. Just follow these steps.

 

Step 1: Tell R8 to "Hands Off!" 🛑

Open your proguard-rules.pro file and add these lines. This tells the compiler: "I know it looks unused, but I need this! Please don't touch it."


# Keep the WorkManager internal database!
-keep class androidx.work.impl.WorkDatabase_Impl { *; }

# Also keep the "Worker" constructors so they can do their jobs
-keep class * extends androidx.work.ListenableWorker {
    <init>(android.content.Context, androidx.work.WorkerParameters);
}

 

Step 2: Give it a Fresh Start ✨

AGP 9 loves caching things. To make sure your new rules are applied:


Click Build > Clean Project.
Click Build > Rebuild Project.

 

Step 3: The "Magic" Re-install 📲

If your app tried to start and failed, it might have left some messy, half-finished files behind. Uninstall the app from your phone/emulator first, then install the new build. This ensures a 100% clean slate!

 

Wrapping Up 🎁

That’s it!

Your app should now be running smoothly even with isMinifyEnabled = true.

The AGP 9 era brings us faster and smaller apps, but it also means we have to be a bit more specific with our ProGuard/R8 rules.

Keep an eye on those "Impl" classes, and you'll be a release-build master in no time!

Happy coding! 💻✨

👉 【Android/AGP9対応】AdMob起因?WorkManagerとApp Startupで頻発するクラッシュをProGuard設定で解決する